/* * JBoss, Home of Professional Open Source * Copyright 2005, JBoss Inc., and individual contributors as indicated * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.security.auth.spi; import java.security.Principal; import java.security.acl.Group; import java.util.Arrays; import java.util.Iterator; import java.util.Properties; import java.util.Map; import java.util.Map.Entry; import javax.management.ObjectName; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.naming.ldap.InitialLdapContext; import javax.security.auth.login.LoginException; import javax.security.auth.Subject; import javax.security.auth.callback.CallbackHandler; import org.jboss.security.PicketBoxLogger; import org.jboss.security.SimpleGroup; import org.jboss.security.vault.SecurityVaultUtil; /** * An implementation of LoginModule that authenticates against an LDAP server * using JNDI, based on the configuration properties. * <p> * The LoginModule options include whatever options your LDAP JNDI provider * supports. Examples of standard property names are: * <ul> * <li><code>Context.INITIAL_CONTEXT_FACTORY = "java.naming.factory.initial"</code> * <li><code>Context.SECURITY_PROTOCOL = "java.naming.security.protocol"</code> * <li><code>Context.PROVIDER_URL = "java.naming.provider.url"</code> * <li><code>Context.SECURITY_AUTHENTICATION = "java.naming.security.authentication"</code> * </ul> * <p> * The Context.SECURITY_PRINCIPAL is set to the distinguished name of the user * as obtained by the callback handler and the Context.SECURITY_CREDENTIALS * property is either set to the String password or Object credential depending * on the useObjectCredential option. * <p> * Additional module properties include: * <ul> * <li>principalDNPrefix, principalDNSuffix : A prefix and suffix to add to the * username when forming the user distinguished name. This is useful if you * prompt a user for a username and you don't want them to have to enter the * fully distinguished name. Using this property and principalDNSuffix the * userDN will be formed as: * <pre> * String userDN = principalDNPrefix + username + principalDNSuffix; * </pre> * <li>useObjectCredential : indicates that the credential should be obtained as * an opaque Object using the <code>org.jboss.security.plugins.ObjectCallback</code> type * of Callback rather than as a char[] password using a JAAS PasswordCallback. * <li>rolesCtxDN : The fixed distinguished name to the context to search for user roles. * <li>userRolesCtxDNAttributeName : The name of an attribute in the user * object that contains the distinguished name to the context to search for * user roles. This differs from rolesCtxDN in that the context to search for a * user's roles can be unique for each user. * <li>uidAttributeID : The name of the attribute that in the object containing * the user roles that corresponds to the userid. This is used to locate the * user roles. * <li>matchOnUserDN : A flag indicating if the search for user roles should match * on the user's fully distinguished name. If false just the username is used * as the match value. If true, the userDN is used as the match value. * <li>allowEmptyPasswords : A flag indicating if empty(length==0) passwords * should be passed to the LDAP server. An empty password is treated as an * anonymous login by some LDAP servers and this may not be a desirable * feature. Set this to false to reject empty passwords, true to have the ldap * server validate the empty password. The default is true. * * <li>roleAttributeIsDN : A flag indicating whether the user's role attribute * contains the fully distinguished name of a role object, or the users's role * attribute contains the role name. If false, the role name is taken from the * value of the user's role attribute. If true, the role attribute represents * the distinguished name of a role object. The role name is taken from the * value of the `roleNameAttributeId` attribute of the corresponding object. In * certain directory schemas (e.g., Microsoft Active Directory), role (group) * attributes in the user object are stored as DNs to role objects instead of * as simple names, in which case, this property should be set to true. * The default value of this property is false. * <li>roleNameAttributeID : The name of the attribute of the role object which * corresponds to the name of the role. If the `roleAttributeIsDN` property is * set to true, this property is used to find the role object's name attribute. * If the `roleAttributeIsDN` property is set to false, this property is ignored. * <li>java.naming.security.principal (4.0.3+): This standard JNDI property if * specified in the login configuration, it is used to rebind to the ldap server * after user authentication for the role searches. This may be necessary if the * user does not have permission to perform these queres. If specified, the * java.naming.security.credentials provides the rebind credentials. * </li> * <li>java.naming.security.credentials (4.0.3+): This standard JNDI property * if specified in the login configuration, it is used to rebind to the LDAP * server after user authentication for the role searches along with the * java.naming.security.principal value. This can be encrypted using the * jaasSecurityDomain. * <li>jaasSecurityDomain (4.0.3+): The JMX ObjectName of the JaasSecurityDomain * to use to decrypt the java.naming.security.principal. The encrypted form * of the password is that returned by the JaasSecurityDomain#encrypt64(byte[]) * method. The org.jboss.security.plugins.PBEUtils can also be used to generate * the encrypted form. * </ul> * A sample login config: * <p> <pre> testLdap { org.jboss.security.auth.spi.LdapLoginModule required java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory java.naming.provider.url="ldap://ldaphost.jboss.org:1389/" java.naming.security.authentication=simple principalDNPrefix=uid= uidAttributeID=userid roleAttributeID=roleName principalDNSuffix=,ou=People,o=jboss.org rolesCtxDN=cn=JBossSX Tests,ou=Roles,o=jboss.org }; testLdap2 { org.jboss.security.auth.spi.LdapLoginModule required java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory java.naming.provider.url="ldap://ldaphost.jboss.org:1389/" java.naming.security.authentication=simple principalDNPrefix=uid= uidAttributeID=userid roleAttributeID=roleName principalDNSuffix=,ou=People,o=jboss.org userRolesCtxDNAttributeName=ou=Roles,dc=user1,dc=com }; testLdapToActiveDirectory { org.jboss.security.auth.spi.LdapLoginModule required java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory java.naming.provider.url="ldap://ldaphost.jboss.org:1389/" java.naming.security.authentication=simple rolesCtxDN=cn=Users,dc=ldaphost,dc=jboss,dc=org uidAttributeID=userPrincipalName roleAttributeID=memberOf roleAttributeIsDN=true roleNameAttributeID=name }; </pre> * * @author Scott.Stark@jboss.org * @version $Revision$ */ @SuppressWarnings("rawtypes") public class LdapLoginModule extends UsernamePasswordLoginModule { // see AbstractServerLoginModule private static final String PRINCIPAL_DN_PREFIX_OPT = "principalDNPrefix"; private static final String PRINCIPAL_DN_SUFFIX_OPT = "principalDNSuffix"; private static final String ROLES_CTX_DN_OPT = "rolesCtxDN"; private static final String USER_ROLES_CTX_DN_ATTRIBUTE_ID_OPT = "userRolesCtxDNAttributeName"; private static final String UID_ATTRIBUTE_ID_OPT = "uidAttributeID"; private static final String ROLE_ATTRIBUTE_ID_OPT = "roleAttributeID"; private static final String MATCH_ON_USER_DN_OPT = "matchOnUserDN"; private static final String ROLE_ATTRIBUTE_IS_DN_OPT = "roleAttributeIsDN"; private static final String ROLE_NAME_ATTRIBUTE_ID_OPT = "roleNameAttributeID"; private static final String SEARCH_TIME_LIMIT_OPT = "searchTimeLimit"; private static final String SEARCH_SCOPE_OPT = "searchScope"; private static final String SECURITY_DOMAIN_OPT = "jaasSecurityDomain"; private static final String ALLOW_EMPTY_PASSWORDS = "allowEmptyPasswords"; private static final String[] ALL_VALID_OPTIONS = { PRINCIPAL_DN_PREFIX_OPT, PRINCIPAL_DN_SUFFIX_OPT, ROLES_CTX_DN_OPT, USER_ROLES_CTX_DN_ATTRIBUTE_ID_OPT, UID_ATTRIBUTE_ID_OPT, ROLE_ATTRIBUTE_ID_OPT, MATCH_ON_USER_DN_OPT, ROLE_ATTRIBUTE_IS_DN_OPT, ROLE_NAME_ATTRIBUTE_ID_OPT, SEARCH_TIME_LIMIT_OPT, SEARCH_SCOPE_OPT, SECURITY_DOMAIN_OPT, ALLOW_EMPTY_PASSWORDS, Context.INITIAL_CONTEXT_FACTORY, Context.OBJECT_FACTORIES, Context.STATE_FACTORIES, Context.URL_PKG_PREFIXES, Context.PROVIDER_URL, Context.DNS_URL, Context.AUTHORITATIVE, Context.BATCHSIZE, Context.REFERRAL, Context.SECURITY_PROTOCOL, Context.SECURITY_AUTHENTICATION, Context.SECURITY_PRINCIPAL, Context.SECURITY_CREDENTIALS, Context.LANGUAGE, Context.APPLET }; public LdapLoginModule() { } @Override public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String,?> sharedState, Map<String,?> options) { addValidOptions(ALL_VALID_OPTIONS); super.initialize(subject, callbackHandler, sharedState, options); } private transient SimpleGroup userRoles = new SimpleGroup("Roles"); /** Overridden to return an empty password string as typically one cannot obtain a user's password. We also override the validatePassword so this is ok. @return and empty password String */ protected String getUsersPassword() throws LoginException { return ""; } /** Overridden by subclasses to return the Groups that correspond to the to the role sets assigned to the user. Subclasses should create at least a Group named "Roles" that contains the roles assigned to the user. A second common group is "CallerPrincipal" that provides the application identity of the user rather than the security domain identity. @return Group[] containing the sets of roles */ protected Group[] getRoleSets() throws LoginException { Group[] roleSets = {userRoles}; return roleSets; } /** Validate the inputPassword by creating a ldap InitialContext with the SECURITY_CREDENTIALS set to the password. @param inputPassword the password to validate. @param expectedPassword ignored */ protected boolean validatePassword(String inputPassword, String expectedPassword) { boolean isValid = false; if (inputPassword != null) { // See if this is an empty password that should be disallowed if (inputPassword.length() == 0) { // Check for an allowEmptyPasswords option boolean allowEmptyPasswords = false; String flag = (String) options.get(ALLOW_EMPTY_PASSWORDS); if (flag != null) allowEmptyPasswords = Boolean.valueOf(flag).booleanValue(); if (allowEmptyPasswords == false) { PicketBoxLogger.LOGGER.traceRejectingEmptyPassword(); return false; } } try { // Validate the password by trying to create an initial context String username = getUsername(); createLdapInitContext(username, inputPassword); isValid = true; } catch (Throwable e) { super.setValidateError(e); } } return isValid; } private void createLdapInitContext(String username, Object credential) throws Exception { Properties env = new Properties(); // Map all option into the JNDI InitialLdapContext env Iterator iter = options.entrySet().iterator(); while (iter.hasNext()) { Entry entry = (Entry) iter.next(); env.put(entry.getKey(), entry.getValue()); } // Set defaults for key values if they are missing String factoryName = env.getProperty(Context.INITIAL_CONTEXT_FACTORY); if (factoryName == null) { factoryName = "com.sun.jndi.ldap.LdapCtxFactory"; env.setProperty(Context.INITIAL_CONTEXT_FACTORY, factoryName); } String authType = env.getProperty(Context.SECURITY_AUTHENTICATION); if (authType == null) env.setProperty(Context.SECURITY_AUTHENTICATION, "simple"); String protocol = env.getProperty(Context.SECURITY_PROTOCOL); String providerURL = (String) options.get(Context.PROVIDER_URL); if (providerURL == null) providerURL = "ldap://localhost:" + ((protocol != null && protocol.equals("ssl")) ? "636" : "389"); String bindDN = (String) options.get(Context.SECURITY_PRINCIPAL); String bindCredential = (String) options.get(Context.SECURITY_CREDENTIALS); String securityDomain = (String) options.get(SECURITY_DOMAIN_OPT); if (securityDomain != null) { ObjectName serviceName = new ObjectName(securityDomain); char[] tmp = DecodeAction.decode(bindCredential, serviceName); bindCredential = new String(tmp); } //Check if the credential is vaultified if(bindCredential != null && SecurityVaultUtil.isVaultFormat(bindCredential)) { bindCredential = SecurityVaultUtil.getValueAsString(bindCredential); } String principalDNPrefix = (String) options.get(PRINCIPAL_DN_PREFIX_OPT); if (principalDNPrefix == null) principalDNPrefix = ""; String principalDNSuffix = (String) options.get(PRINCIPAL_DN_SUFFIX_OPT); if (principalDNSuffix == null) principalDNSuffix = ""; String matchType = (String) options.get(MATCH_ON_USER_DN_OPT); boolean matchOnUserDN = Boolean.valueOf(matchType).booleanValue(); String userDN = principalDNPrefix + username + principalDNSuffix; env.setProperty(Context.PROVIDER_URL, providerURL); env.setProperty(Context.SECURITY_PRINCIPAL, userDN); env.put(Context.SECURITY_CREDENTIALS, credential); this.traceLDAPEnv(env); InitialLdapContext ctx = null; ClassLoader currentTCCL = SecurityActions.getContextClassLoader(); try { if (currentTCCL != null) SecurityActions.setContextClassLoader(null); ctx = new InitialLdapContext(env, null); if (PicketBoxLogger.LOGGER.isTraceEnabled()) { PicketBoxLogger.LOGGER.traceSuccessfulLogInToLDAP(ctx.toString()); } if (bindDN != null) { try { ctx.close(); } catch (NamingException e) { PicketBoxLogger.LOGGER.warnProblemClosingOriginalLdapContextDuringRebind(e); } // Rebind the ctx to the bind dn/credentials for the roles searches PicketBoxLogger.LOGGER.traceRebindWithConfiguredPrincipal(bindDN); env.setProperty(Context.SECURITY_PRINCIPAL, bindDN); env.put(Context.SECURITY_CREDENTIALS, bindCredential); ctx = new InitialLdapContext(env, null); } /* If a userRolesCtxDNAttributeName was speocified, see if there is a user specific roles DN. If there is not, the default rolesCtxDN will be used. */ String rolesCtxDN = (String) options.get(ROLES_CTX_DN_OPT); String userRolesCtxDNAttributeName = (String) options.get(USER_ROLES_CTX_DN_ATTRIBUTE_ID_OPT); if (userRolesCtxDNAttributeName != null) { // Query the indicated attribute for the roles ctx DN to use String[] returnAttribute = {userRolesCtxDNAttributeName}; try { Attributes result = ctx.getAttributes(userDN, returnAttribute); if (result.get(userRolesCtxDNAttributeName) != null) { rolesCtxDN = result.get(userRolesCtxDNAttributeName).get().toString(); PicketBoxLogger.LOGGER.traceFoundUserRolesContextDN(rolesCtxDN); } } catch (NamingException e) { PicketBoxLogger.LOGGER.debugFailureToQueryLDAPAttribute(userRolesCtxDNAttributeName, userDN, e); } } // Search for any roles associated with the user if (rolesCtxDN != null) { String uidAttrName = (String) options.get(UID_ATTRIBUTE_ID_OPT); if (uidAttrName == null) uidAttrName = "uid"; String roleAttrName = (String) options.get(ROLE_ATTRIBUTE_ID_OPT); if (roleAttrName == null) roleAttrName = "roles"; StringBuffer roleFilter = new StringBuffer("("); roleFilter.append(uidAttrName); roleFilter.append("={0})"); String userToMatch = username; if (matchOnUserDN == true) userToMatch = userDN; String[] roleAttr = {roleAttrName}; // Is user's role attribute a DN or the role name String roleAttributeIsDNOption = (String) options.get(ROLE_ATTRIBUTE_IS_DN_OPT); boolean roleAttributeIsDN = Boolean.valueOf(roleAttributeIsDNOption).booleanValue(); // If user's role attribute is a DN, what is the role's name attribute // Default to 'name' (Group name attribute in Active Directory) String roleNameAttributeID = (String) options.get(ROLE_NAME_ATTRIBUTE_ID_OPT); if (roleNameAttributeID == null) roleNameAttributeID = "name"; int searchScope = SearchControls.SUBTREE_SCOPE; int searchTimeLimit = 10000; String timeLimit = (String) options.get(SEARCH_TIME_LIMIT_OPT); if (timeLimit != null) { try { searchTimeLimit = Integer.parseInt(timeLimit); } catch (NumberFormatException e) { PicketBoxLogger.LOGGER.debugFailureToParseNumberProperty(SEARCH_TIME_LIMIT_OPT, searchTimeLimit); } } String scope = (String) options.get(SEARCH_SCOPE_OPT); if ("OBJECT_SCOPE".equalsIgnoreCase(scope)) searchScope = SearchControls.OBJECT_SCOPE; else if ("ONELEVEL_SCOPE".equalsIgnoreCase(scope)) searchScope = SearchControls.ONELEVEL_SCOPE; if ("SUBTREE_SCOPE".equalsIgnoreCase(scope)) searchScope = SearchControls.SUBTREE_SCOPE; NamingEnumeration answer = null; try { SearchControls controls = new SearchControls(); controls.setSearchScope(searchScope); controls.setReturningAttributes(roleAttr); controls.setTimeLimit(searchTimeLimit); Object[] filterArgs = {userToMatch}; if (PicketBoxLogger.LOGGER.isTraceEnabled()) { PicketBoxLogger.LOGGER.traceRolesDNSearch(rolesCtxDN, roleFilter.toString(), userToMatch, Arrays.toString(roleAttr), searchScope, searchTimeLimit); } answer = ctx.search(rolesCtxDN, roleFilter.toString(), filterArgs, controls); while (answer.hasMore()) { SearchResult sr = (SearchResult) answer.next(); PicketBoxLogger.LOGGER.traceCheckSearchResult(sr.getName()); Attributes attrs = sr.getAttributes(); Attribute roles = attrs.get(roleAttrName); if (roles != null) { for (int r = 0; r < roles.size(); r++) { Object value = roles.get(r); String roleName = null; if (roleAttributeIsDN == true) { // Query the roleDN location for the value of roleNameAttributeID String roleDN = value.toString(); String[] returnAttribute = {roleNameAttributeID}; PicketBoxLogger.LOGGER.traceFollowRoleDN(roleDN); try { Attributes result2 = ctx.getAttributes(roleDN, returnAttribute); Attribute roles2 = result2.get(roleNameAttributeID); if (roles2 != null) { for (int m = 0; m < roles2.size(); m++) { roleName = (String) roles2.get(m); addRole(roleName); } } } catch (NamingException e) { PicketBoxLogger.LOGGER.debugFailureToQueryLDAPAttribute(roleNameAttributeID, roleDN, e); } } else { // The role attribute value is the role name roleName = value.toString(); addRole(roleName); } } } else { PicketBoxLogger.LOGGER.debugFailureToFindAttrInSearchResult(roleAttrName, sr.getName()); } } } catch (NamingException e) { PicketBoxLogger.LOGGER.debugFailureToExecuteRolesDNSearch(e); } finally { if (answer != null) answer.close(); } } } finally { // Close the context to release the connection if (ctx != null) ctx.close(); if (currentTCCL != null) SecurityActions.setContextClassLoader(currentTCCL); } } private void addRole(String roleName) { if (roleName != null) { try { Principal p = super.createIdentity(roleName); PicketBoxLogger.LOGGER.traceAssignUserToRole(roleName); userRoles.addMember(p); } catch (Exception e) { PicketBoxLogger.LOGGER.debugFailureToCreatePrincipal(roleName, e); } } } /** * <p> * Logs the specified LDAP env, masking security-sensitive information (passwords). * </p> * * @param env the LDAP env to be logged. */ private void traceLDAPEnv(Properties env) { Properties tmp = new Properties(); tmp.putAll(env); if (tmp.containsKey(Context.SECURITY_CREDENTIALS)) tmp.setProperty(Context.SECURITY_CREDENTIALS, "******"); PicketBoxLogger.LOGGER.traceLDAPConnectionEnv(tmp); } }