(zos)Custom one to one LDAP to System Authorization Facility (SAF) mapping modules
We can customize Java Authentication and Authorization (JAAS) login configurations by writing a customized login mapping module.
WebSphere Application Server for z/OS has the ability to provide mapping from a remote method invocation (RMI) inbound request with LDAP credentials to the system authorization facility (SAF) Identity. The use case is a WAS on any platform configured to LDAP and sends a RMI/IIOP request to a second server configured to a SAF User Registry. The WebSphere Application Server (release 5.1 or later) sends a RMI request using LTPA Token representing the LDAP identity to the WAS for z/OS configured to SAF. The following figure illustrates this mapping.
Figure 1. Mapping from RMI inbound request with LDAP credentials to SAF Identity
The sample below is a JAAS RMI inbound login module that opens the LTPA token, extracts the LDAP userid, strips the cn=userID, and sets the WSCredential to the SAF userid. The sample inbound login module configured to the second server is only supported on WebSphere Application Server for z/OS release 6.1 or later.
The sample is really a hash login module in which the com.ibm.wsspi.security.token.AttributeNameConstants.WSCREDENTIAL_UNIQUEID is set to the desired z/OS SAF Identity. TheltpaLoginModule checks for a com.ibm.wsspi.security.token.AttributeNameConstants.WSCREDENTIAL_UNIQUEID that has been passed and uses the ID specified to build the appropriate security context for the user.
This sample module is configured as a RMI_INBOUND JAAS login module and must be specified at the top of the list followed by the ltpaLoginModule.
In the following sample, a Java EE identity is available to control the mapping to a SAF identity. The sample uses the hash login of Constants.WSCREDENTIAL value in the shared state.
The following code is a sample SampleLTPASAFMappingModule program. We can use this sample program "as is" or modify, compile, and install it in the WAS_HOME/classes directory.
// This program may be used, executed, 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; import com.ibm.wsspi.security.token.WSSecurityPropagationHelper; import com.ibm.websphere.security.auth.callback.WSCredTokenCallbackImpl; import com.ibm.websphere.security.auth.WSLoginFailedException; /** * ** * SampleLTPASAFMappingModule demonstrates a custom login module * that maps the existing LTPA Token from the shared state to a UNIQUE ID to avoid the Login. * Typical use is a inbound LTPA Token generated from a different realm (perhaps a non z/OS Realm) * into a SAF User Registry. * The LTPA Token is used as the trusted identity token. * * *** * * @author IBM Corporation * @version 1.0 * @since 1.0 */ public class SampleLTPASAFMappingModule 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.SampleLTPASAFMappingModule"; private final static int MAXIMUM_NAME_LENGTH = 8; /* * ** * 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; /* * ** * Inbound realm that this Login Module is to handle. If there is no incoming * property setting for onlyThisRealm, then this Login module will process all inbound * request regardless of the incoming idenity realm within the LTPA token. * *** */ private String onlyThisRealm = null; /* * ** * 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 SampleLTPASAFMappingModule() { } /** * **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"); } // obtain the value for realm before anything else to see if this login module should // only process a particular inbound realm or if it should process all realm. A null value // singles all realms will be processed. if (newOptions.containsKey("realm")) { onlyThisRealm = (String) newOptions.get("realm"); onlyThisRealm = onlyThisRealm.trim(); } // 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 (debugEnabled) { debug(new Object[] { "initialize() exit", subject, callbackHandler, sharedState, options }); } } /** * * @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"); } // Handle the callback javax.security.auth.callback.Callback callbacks[] = new javax.security.auth.callback.Callback[1]; callbacks[0] = new com.ibm.websphere.security.auth.callback.WSCredTokenCallbackImpl(""); try { callbackHandler.handle(callbacks); } catch (Exception e) { if (debugEnabled) { debug(new Object[] { "Caught exception in callbackhandler: ", e }); } return false; } byte[] credToken = ((WSCredTokenCallbackImpl) callbacks[0]).getCredToken(); String uid = null; String realm = null; String uniqueID = null; //This routine will only process a LTPA token. If there is no inbound token, //then this routine will return true and allow the other LM should handle //this request if (credToken != null) { try { uniqueID = WSSecurityPropagationHelper.validateLTPAToken(credToken); realm = WSSecurityPropagationHelper.getRealmFromUniqueID (uniqueID); uid = createSAFIdenityName (uniqueID); if (debugEnabled) { debug("using uniqueID: "+ uniqueID+ " inbound realm: "+ realm + "uid: " + uid ); } } catch (Exception e) { if (debugEnabled) { debug(new Object[] { "Caught exception in callbackhandler: ", e }); } return false; } } else return true; // let the other LM handle this request. //onlyThisRealm is a input property setting to see if we want this login module //to handle only 1 inputed realm request. If onlyThisRealm is null, //then we will process all request and continue on. If the onlyThisRealm does //not match the inbound request extracted from LTPA, then we will let the other //LM handle this request. // if ((onlyThisRealm != null) && (!realm.trim().equals(onlyThisRealm))) { if (debugEnabled) { debug("inbound realm of "+realm+" does not match option realm of "+onlyThisRealm); } return true; } try { // Retrieves the default InitialContext for this server. javax.naming.InitialContext ctx = new javax.naming.InitialContext(); // Retrieves the local UserRegistry object. com.ibm.websphere.security.UserRegistry reg = (com.ibm.websphere.security.UserRegistry) ctx.lookup("UserRegistry"); // Retrieves the registry uniqueID based on the uid specified // in the NameCallback. String uniqueid = reg.getUniqueUserId(uid); if (debugEnabled) { debug("uniqueid "+uniqueid); } // Retrieves the display name from the user registry based on the uniqueID. String securityName = reg.getUserSecurityName(uid); if (debugEnabled) { debug("securityName "+securityName); } // Retrieves the groups associated with this uniqueID. java.util.List groupList = reg.getUniqueGroupIds(uid); // Creates the java.util.Hashtable with the information that you gathered // from the UserRegistry. // By setting this hashtable, the LTPA login module will propertly // setup the SAF Identity. java.util.Hashtable hashtable = new java.util.Hashtable(); hashtable.put(com.ibm.wsspi.security.token.AttributeNameConstants. WSCREDENTIAL_UNIQUEID, uniqueid); hashtable.put(com.ibm.wsspi.security.token.AttributeNameConstants. WSCREDENTIAL_SECURITYNAME, securityName); hashtable.put(com.ibm.wsspi.security.token.AttributeNameConstants. WSCREDENTIAL_GROUPS, groupList); // Adds the hashtable to the shared state of the Subject. sharedState.put(com.ibm.wsspi.security.token.AttributeNameConstants. WSCREDENTIAL_PROPERTIES_KEY, hashtable); } catch (Exception e) { if (debugEnabled) { debug(new Object[] { "Caught exception in callbackhandler: ", e }); } WSLoginFailedException e2 = new WSLoginFailedException("SampleLTPASAFMappingModule detected an error. "+e); throw e2; } 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); } } } /* * ** * TODO * Private method to generate the SAF Idenity from the LDAP identity. This routine may * need to be modify by the installation standards to extract the SAF Idenity from * the LDAP IDentity. OR this routine could be written to map the LDAP to SAF identity. * *** */ private String createSAFIdenityName(String name) { if (debugEnabled) { debug("createSAFIdenityName() entry"); } if (debugEnabled) { debug("Using name='" + name + "' from principal"); } name = name.toUpperCase(); int index = name.indexOf("/") + 1; // index of the first character after the first / System.out.println(index); System.out.println(name); 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 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); } System.out.println(name); if (name.indexOf("CN=") >= 0) name = name.substring((name.indexOf("CN=")+3),name.length()); if (name.indexOf(",") > 0) name = name.substring(0,name.indexOf(",")); // 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); } } if (debugEnabled) { debug("createSAFIdenityName() exit"); } return name; //return a SAF Idenity Name } }The sample mapping module creates a mapping from an RMI inbound request with LDAP credentials to SAF identity. This module can be customized to perform the mapping.
Related tasks
Develop programmatic logins with the Java Authentication and Authorization Service Writing a custom System Authorization Facility (SAF) mapping module with non-local operating system Configure a custom System Authorization Facility mapping module for WebSphere Application Server Develop custom login modules for a system login configuration for JAAS