(ZOS) Custom System Authorization Facility mapping modules
We can customize Java Authentication and Authorization (JAAS) login configurations by writing a customized login mapping module.
If we are using the SAF distributed identity mapping feature, we do not need to configure a mapping module.
The WebSphere Application Server ltpaLoginModule module and the AuthenLoginModule module use the shared state to save state information with the capability to allow LoginModules can modify state information. The ltpaLoginModule initializes the callback array in the login() method using the following code. The callback array is created by ltpaLoginModule only if an array is not defined in the shared state area.
In the following code example, the reliance is made on the availability of a Java EE identity to control the mapping to a System Authorization Facility (SAF) identity. This code uses the Constants.WSPRINCIPAL_KEY value in the shared state. The value is placed in the code by a WAS 1 login module. We can insert a custom LoginModule after the ltpaLoginModule module, and just before the MapPlatformSubject module. If we do this, use a callback array or other shared state values to obtain a value used to control the mapping to the z/OS user ID.
The following is a sample SAFMappingModule:
// // This program may be used, run, copied, modified and // distributed without royalty for the purpose of developing, // using, marketing, or distributing. // // package com.ibm.websphere.security; import com.ibm.websphere.security.auth.CredentialDestroyedException; import com.ibm.websphere.security.auth.WSPrincipal; import com.ibm.websphere.security.cred.WSCredential; import com.ibm.wsspi.security.auth.callback.Constants; import com.ibm.wsspi.security.token.AttributeNameConstants; import java.lang.reflect.Array; import java.util.Map; import javax.security.auth.Subject; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.login.CredentialExpiredException; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; /** * * SampleSAFMappingModule demonstrates a custom login module * that maps the existing WSPrincipal from the shared state to a z/OS * user id. * * * * The following values will be set into the shared state if authentication * succeeds. If authentication fails, this login module will still indicate * success, but no values are set into the shared state. * * AttributeNameConstants.ZOS_USERID * AttributeNameConstants.ZOS_AUDIT_STRING * AttributeNameConstants.CALLER_PRINCIPAL_CLASS * * This login module does not use any callbacks, nor does it modify the Subject * in any way. * * @author IBM Corporation * @version 1.0 * @since 1.0 */ public class SampleSAFMappingModule implements LoginModule { /* * Constant that represents the name of this mapping module. Whenever this sample * code is used to create a class with a different name, this value should be changed. * * By default, this value is used as part of the sample audit token, and for debugging * purposes. */ private final static String MAPPING_MODULE_NAME = "com.ibm.websphere.security.SampleSAFMappingModule"; /* * Constant that represents the maximum length of the ZOS_USERID. Current MVS naming * restrictions limit this to eight characters. * * When the option to useWSPrincipalName is chosen, the name from the WSPrincipal is * shortened to this many characters. */ private final static int MAXIMUM_NAME_LENGTH = 8; /* * Specifies whether or not to use this module's default mapping behavior, which is * to use the WSPrincipal name to generate the ZOS_USERID. This depends on the * value of the "useWSPrincipalName" option passed in from the LoginContext. */ private boolean useWSPrincipalName = true; /* * Specifies whether debugging is enabled for this Login Module. This depends on the * value of the "debug" option passed in from the LoginContext. */ private boolean debugEnabled = false; /* * Stores the Subject passed from the LoginContext. */ private Subject subject; /* * Stores the CallbackHandler passed from the LoginContext. */ private CallbackHandler callbackHandler; /* * Stores the shared state Map passed from the LoginContext. */ private Map sharedState; /* * Stores the options Map passed from the LoginContext. */ private Map options; /* * This value is used to store the success or failure of the login() method so * that commit() and abort() can act differently in the two cases if so desired. */ private boolean succeeded = false; /** * Construct an uninitialized mapping module object. */ public SampleSAFMappingModule() { } /** * Initialize this login module. * * This is called by the LoginContext after this login module is * instantiated. The relevant information is passed from the LoginContext * to this login module. If the login module does not understand any of the data * stored in the sharedState and options parameters, * they can be ignored. * * @param subject * The subject that this LoginContext is authenticating * @param callbackHandler * A CallbackHandler for communicating with the end user * to gather login information (e.g., username and password). * @param sharedState * The state shared with other configured login modules. * @param options * The options specified in the login configuration for this particular login module. */ public void initialize(Subject newSubject, CallbackHandler newCallbackHandler, Map newSharedState, Map newOptions) { // obtain the value for debug before anything else so that tracing can be used within // this method if (newOptions.containsKey("debug")) { String debugEnabledString = (String) newOptions.get("debug"); if (debugEnabledString != null && debugEnabledString.toLowerCase().equals("true")) { debugEnabled = true; } } if (debugEnabled) { debug("initialize() entry"); } // this login module is not going to use any of these objects except for the sharedState, // but for consistency with most login modules, we will save a reference to all of them this.subject = newSubject; this.callbackHandler = newCallbackHandler; this.sharedState = newSharedState; this.options = newOptions; if (options.containsKey("useWSPrincipalName")) { String useWSPrincipalNameString = (String) options.get("useWSPrincipalName"); if (useWSPrincipalNameString != null && useWSPrincipalNameString.toLowerCase().equals("false")) { useWSPrincipalName = false; } } if (debugEnabled) { debug(new Object[] { "initialize() exit", subject, callbackHandler, sharedState, options }); } } /** * Method to map the WSPrincipal to a ZOS_USERID * * This method derives a ZOS_USERID and stores it into the Shared State for use by a later * Login Module. * * @return true if the authentication succeeded, or false * if this Login Module should be ignored * @exception LoginException * if the authentication fails, which is impossible for this Login Module */ public boolean login() throws LoginException { if (debugEnabled) { debug("login() entry"); } if (sharedState.containsKey(AttributeNameConstants.ZOS_USERID)) { // we don't want to override this value if, for whatever reason, another Login Module // has already placed it into the shared state, but we still consider this a success // because the exit criteria for this module has been met if (debugEnabled) { debug("ZOS_USERID already exists: so no additional work is needed"); } succeeded = true; } else if (!sharedState.containsKey(Constants.WSPRINCIPAL_KEY) || !sharedState.containsKey(Constants.WSCREDENTIAL_KEY)) { // if there isn't a Principal or Credential in the shared state, we can't do // anything so we'll return false to inform the LoginContext to ignore this module if (debugEnabled) { debug("Principal or Credential is unavailable: skipping this Login Module"); } succeeded = false; } else { if (debugEnabled) { debug("Principal and Credential are available: continue with login"); } String name = null; String audit = null; String principalClass = null; // extract the WSPrincipal and WSCredential from the shared state WSPrincipal principal = (WSPrincipal) sharedState.get(Constants.WSPRINCIPAL_KEY); WSCredential credential = (WSCredential) sharedState.get(Constants.WSCREDENTIAL_KEY); if (useWSPrincipalName) { // this sample mapping module provides a method to obtain the ZOS_USERID directly // from the WSPrincipal name if the property "useWSPrincipalName" is set to true if (debugEnabled) { debug("Using name from WSPrincipal to obtain ZOS_USERID"); } name = createName(principal); String realm = getRealm(credential); // for this example, a sample audit token will be created that combines the ZOS_USERID, // the realm, and the name of this mapping module // // a custom audit token can be created using any available data rather than using // this sample token audit = realm + "/" + name + " MappingModule:" + MAPPING_MODULE_NAME; // A Subject may contain more than one Principal. This value specifies the // class of the Principal to be returned when the Subject is asked for the // Caller Principal. If a custom Principal class has been placed into the // Subject, that class name can be specified here. // // Two predefined values for the Caller Principal Class are provided: // // - AttributeNameConstants.ZOS_CALLER_PRINCIPAL_CLASS // the z/OS Principal class // // - AttributeNameConstants.DEFAULT_CALLER_PRINCIPAL_CLASS // the default Principal class principalClass = AttributeNameConstants.DEFAULT_CALLER_PRINCIPAL_CLASS; succeeded = true; } else { if (debugEnabled) { debug("Using Custom logic to obtain ZOS_USERID"); } // if the behavior provided by this mapping module to obtain the ZOS_USERID from the // WSPrincipal name is not enough, custom mapping logic can be provided here // // to use this custom mapping logic, the property "useWSPrincipalName" must be set // to false // name = ...custom logic // audit = ...custom logic // principalClass = ...custom logic // by default, no custom mapping is provided, so the success of this code path // is false; if custom mapping is provided, the following variable should be // modified to represent the success or failure of the custom mapping succeeded = false; } if (succeeded) { // now that we have values for name, audit, and principalClass, we just need to set // them into the shared state sharedState.put(AttributeNameConstants.ZOS_USERID, name); sharedState.put(AttributeNameConstants.ZOS_AUDIT_STRING, audit); sharedState.put(AttributeNameConstants.CALLER_PRINCIPAL_CLASS, principalClass); if (debugEnabled) { debug(new Object[] { "Values have been stored into the shared state", name, audit, principalClass }); } } } if (debugEnabled) { debug("login() exit"); } return succeeded; } /** * Method to commit the authentication result. * * This Login Module does not need to commit any data, so we will simply return. * * @return true if the original login succeeded, or false * if the original login failed * @exception LoginException * if the commit fails, which cannot happen in this Login Module */ public boolean commit() throws LoginException { if (debugEnabled) { debug("commit() entry"); } // the return value of commit() is the same as the success of the original login boolean returnVal = succeeded; cleanup(); if (debugEnabled) { debug("commit() exit"); } return returnVal; } /** * Method to abort the authentication process (Phase 2). * * No matter whether our original login succeeded or failed, this method cleans up * our state and returns. * * @return true if the original login succeeded, or false * if the original login failed * @exception LoginException * if the abort fails, which cannot happen in this Login Module */ public boolean abort() throws LoginException { if (debugEnabled) { debug("abort() entry"); } // the return value of abort() is the same as the success of the original login boolean returnVal = succeeded; cleanup(); if (debugEnabled) { debug("abort() exit"); } return returnVal; } /** * Method which logs out a Subject. * * Since our commit method did not modify the Subject, we don't have anything to * logout or clean up and can just return true. * * @return true if the logout succeeded * @exception LoginException * if the logout fails, which cannot happen in the Login Module */ public boolean logout() throws LoginException { if (debugEnabled) { debug("logout() entry"); } // our local variables were cleanup up during the commit, so no further cleanup is needed if (debugEnabled) { debug("logout() exit"); } // since there is nothing to logout, we always succeed return true; } /* * Cleans up our local variables; the only cleanup required for * this Login Module is to set our success variable back to false. */ private void cleanup() { if (debugEnabled) { debug("cleanup() entry"); } // there's nothing to cleanup, really, so just reset our success variable succeeded = false; if (debugEnabled) { debug("cleanup() exit"); } } /* * Private method to print trace information. This implementation uses System.out * to print trace information to standard output, but a custom tracing system can * be implemented here as well. */ private void debug(Object o) { System.out.println("Debug: " + MAPPING_MODULE_NAME); if (o != null) { if (o.getClass().isArray()) { int length = Array.getLength(o); for (int i = 0; i < length; i++) { System.out.println("\t" + Array.get(o, i)); } } else { System.out.println("\t" + o); } } } /* * Private method to obtain the realm name from the Credential and return it. This * keeps the exception handling involved with obtaining the realm name out of the main * login() logic. */ private String getRealm(WSCredential credential) { if (debugEnabled) { debug("getRealm() entry"); } String realm = null; try { realm = credential.getRealmName(); if (debugEnabled) { debug("Got realm='" + realm + "' from credential"); } } catch (Exception e) { // GetRealmName throws CredentialExpiredException and CredentialDestroyedException if (debugEnabled) { debug(new Object[] { "Caught exception in getRealm: ", e }); } realm = "UNKNOWN_REALM"; } if (debugEnabled) { debug("getRealm() exit"); } return realm; } /* * Private method to generate the ZOS_USERID from the WSPrincipal name. */ private String createName(WSPrincipal principal) { if (debugEnabled) { debug("createName() entry"); } String name = principal.getName(); if (debugEnabled) { debug("Using name='" + name + "' from principal"); } // WSPrincipal.getName() might return REALM/NAME, so parse the String to obtain just the name int index = name.indexOf("/") + 1; // index of the first character after the first / if (index >= name.length()) { // this block handles the case where the first / is the last character in the String, // it really shouldn't happen, but if it does we can just strip it off name = name.substring(0, index - 1); if (debugEnabled) { debug("Stripping trailing / from WSPrincipal name"); } } else { // index is either 0 (if no / exists in the name), or it is the position // after the first / in the name // // either way, we will take the substring from that point until the end of the string name = name.substring(index); } // shorten the name if its length exceeds the defined maximum if (name.length() > MAXIMUM_NAME_LENGTH) { name = name.substring(0, MAXIMUM_NAME_LENGTH); if (debugEnabled) { debug("WSPrincipal name shortened to " + name); } } // MVS ids are all upper case name = name.toUpperCase(); if (debugEnabled) { debug("createName() exit"); } return name; } }The sample mapping module creates a mapping between the Lightweight Directory Access Protocol (LDAP) identity and the z/OS identity. The LDAP identity is used as the z/OS user ID. If a different mapping is required this module can be customized (as shown in the else clause) to perform the mapping. Then:
- Compile the Java code. Verify the code is trusted and treated with the same care as an APF-authorized module. The default Java Authorization and Authentication Service (JAAS) System login configuration is accessed from the z/OS controller.
- If we specify a mapping class other than the default supplied by IBM, install the class into the classes directory of the Application Server and Deployment Managers. Place the JAR file into the WAS_HOME/classes directory for each node in the cell, including the deployment manager node in a WAS ND cell.
Related:
Distributed identity mapping using SAF Writing a custom System Authorization Facility (SAF) mapping module with non-local operating system Install and configuring a custom System Authorization Facility mapping module for WAS