/** * Copyright (c) 2009 Juwi MacMillan Group GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.juwimm.cms.authorization.jaas.ldap; import java.security.Principal; import java.security.acl.Group; import java.util.Iterator; import java.util.Properties; 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 org.jboss.security.SimpleGroup; /** * 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 distiguished 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 necessar 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: 57203 $ */ public class LdapLoginModule extends UsernamePasswordLoginModule { 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"; public LdapLoginModule() { } private transient final SimpleGroup userRoles = new SimpleGroup("Roles"); /** Overriden 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 */ @Override protected String getUsersPassword() throws LoginException { return ""; } /** Overriden 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 */ @Override 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 */ @Override 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 = true; String flag = (String) options.get("allowEmptyPasswords"); if (flag != null) allowEmptyPasswords = Boolean.valueOf(flag).booleanValue(); if (allowEmptyPasswords == false) { super.log.trace("Rejecting empty password due to allowEmptyPasswords"); 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 { boolean trace = log.isTraceEnabled(); 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); } 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); if (trace) { Properties tmp = new Properties(); tmp.putAll(env); tmp.setProperty(Context.SECURITY_CREDENTIALS, "***"); log.trace("Logging into LDAP server, env=" + tmp.toString()); } InitialLdapContext ctx = new InitialLdapContext(env, null); if (trace) log.trace("Logged into LDAP server, " + ctx); if (bindDN != null) { // Rebind the ctx to the bind dn/credentials for the roles searches if (trace) log.trace("Rebind SECURITY_PRINCIPAL to: " + 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(); log.trace("Found user roles context DN: " + rolesCtxDN); } } catch (NamingException e) { if (log.isDebugEnabled()) log.debug("Failed to query userRolesCtxDNAttributeName", 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) { log.trace("Failed to parse: " + timeLimit + ", using searchTimeLimit=" + 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; try { SearchControls controls = new SearchControls(); controls.setSearchScope(searchScope); controls.setReturningAttributes(roleAttr); controls.setTimeLimit(searchTimeLimit); Object[] filterArgs = {userToMatch}; if (trace) { log.trace("searching rolesCtxDN=" + rolesCtxDN + ", roleFilter=" + roleFilter + ", filterArgs=" + userToMatch + ", roleAttr=" + roleAttr + ", searchScope=" + searchScope + ", searchTimeLimit=" + searchTimeLimit); } NamingEnumeration answer = ctx.search(rolesCtxDN, roleFilter.toString(), filterArgs, controls); while (answer.hasMore()) { SearchResult sr = (SearchResult) answer.next(); if (trace) { log.trace("Checking answer: " + sr.getName()); } Attributes attrs = sr.getAttributes(); Attribute roles = attrs.get(roleAttrName); 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}; if (trace) log.trace("Following roleDN: " + 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) { log.trace("Failed to query roleNameAttrName", e); } } else { // The role attribute value is the role name roleName = value.toString(); addRole(roleName); } } } answer.close(); } catch (NamingException e) { if (trace) log.trace("Failed to locate roles", e); } } // Close the context to release the connection ctx.close(); } private void addRole(String roleName) { if (roleName != null) { try { Principal p = super.createIdentity(roleName); log.trace("Assign user to role " + roleName); userRoles.addMember(p); } catch (Exception e) { if (log.isDebugEnabled()) log.debug("Failed to create principal: " + roleName, e); } } } }