/* * JBoss, Home of Professional Open Source. * Copyright 2011, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file 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.acl.Group; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; 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.Subject; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.login.LoginException; import org.jboss.security.PicketBoxLogger; import org.jboss.security.PicketBoxMessages; import org.jboss.security.Util; /** * A login module to authenticate users using a LDAP server. * * @author <a href="mmoyses@redhat.com">Marcus Moyses</a> * @author Andy Oliver * @author Scott.Stark@jboss.org */ public class LdapUsersLoginModule extends UsernamePasswordLoginModule { // see AbstractServerLoginModule private static final String BIND_DN = "bindDN"; private static final String BIND_CREDENTIAL = "bindCredential"; private static final String BASE_CTX_DN = "baseCtxDN"; private static final String BASE_FILTER_OPT = "baseFilter"; private static final String SEARCH_TIME_LIMIT_OPT = "searchTimeLimit"; private static final String SEARCH_SCOPE_OPT = "searchScope"; private static final String DISTINGUISHED_NAME_ATTRIBUTE_OPT = "distinguishedNameAttribute"; private static final String PARSE_USERNAME = "parseUsername"; private static final String USERNAME_BEGIN_STRING = "usernameBeginString"; private static final String USERNAME_END_STRING = "usernameEndString"; private static final String ALLOW_EMPTY_PASSWORDS = "allowEmptyPasswords"; private static final String[] ALL_VALID_OPTIONS = { BIND_DN,BIND_CREDENTIAL,BASE_CTX_DN,BASE_FILTER_OPT, SEARCH_TIME_LIMIT_OPT,SEARCH_SCOPE_OPT, DISTINGUISHED_NAME_ATTRIBUTE_OPT, PARSE_USERNAME,USERNAME_BEGIN_STRING,USERNAME_END_STRING,ALLOW_EMPTY_PASSWORDS, Context.INITIAL_CONTEXT_FACTORY,Context.SECURITY_AUTHENTICATION,Context.SECURITY_PROTOCOL, Context.PROVIDER_URL,Context.SECURITY_PRINCIPAL,Context.SECURITY_CREDENTIALS }; protected String bindDN; protected String bindCredential; protected String baseDN; protected String baseFilter; protected int searchTimeLimit = 10000; protected int searchScope = SearchControls.SUBTREE_SCOPE; protected String distinguishedNameAttribute; protected boolean parseUsername; protected String usernameBeginString; protected String usernameEndString; protected boolean allowEmptyPasswords; @Override protected String getUsersPassword() throws LoginException { return ""; } @Override protected Group[] getRoleSets() throws LoginException { return new Group[0]; } @Override protected String getUsername() { String username = super.getUsername(); if (parseUsername) { int beginIndex = 0; if (usernameBeginString != null && !usernameBeginString.equals("")) beginIndex = username.indexOf(usernameBeginString) + usernameBeginString.length(); if (beginIndex == -1) // not allowed. reset beginIndex = 0; int endIndex = username.length(); if (usernameEndString != null && !usernameEndString.equals("")) endIndex = username.substring(beginIndex).indexOf(usernameEndString); if (endIndex == -1) // not allowed. reset endIndex = username.length(); else endIndex += beginIndex; username = username.substring(beginIndex, endIndex); } return username; } @Override public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) { addValidOptions(ALL_VALID_OPTIONS); super.initialize(subject, callbackHandler, sharedState, options); bindDN = (String) options.get(BIND_DN); bindCredential = (String) options.get(BIND_CREDENTIAL); if ((bindCredential != null) && Util.isPasswordCommand(bindCredential)) { try { bindCredential = new String(Util.loadPassword(bindCredential)); } catch (Exception e) { throw PicketBoxMessages.MESSAGES.failedToDecodeBindCredential(e); } } baseDN = (String) options.get(BASE_CTX_DN); baseFilter = (String) options.get(BASE_FILTER_OPT); 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; distinguishedNameAttribute = (String) options.get(DISTINGUISHED_NAME_ATTRIBUTE_OPT); if (distinguishedNameAttribute == null) distinguishedNameAttribute = "distinguishedName"; allowEmptyPasswords = Boolean.valueOf((String) options.get(ALLOW_EMPTY_PASSWORDS)); parseUsername = Boolean.valueOf((String) options.get(PARSE_USERNAME)); if (parseUsername) { usernameBeginString = (String) options.get(USERNAME_BEGIN_STRING); usernameEndString = (String) options.get(USERNAME_END_STRING); } } @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) { if (allowEmptyPasswords == false) { PicketBoxLogger.LOGGER.traceRejectingEmptyPassword(); return false; } } try { // Validate the password by trying to create an initial context String username = getUsername(); isValid = createLdapInitContext(username, inputPassword); } catch (Throwable e) { super.setValidateError(e); } } return isValid; } /** * Bind to the LDAP server for authentication */ private boolean createLdapInitContext(String username, Object credential) throws Exception { // Get the admin context for searching InitialLdapContext ctx = null; ClassLoader currentTCCL = SecurityActions.getContextClassLoader(); try { if (currentTCCL != null) SecurityActions.setContextClassLoader(null); ctx = constructInitialLdapContext(bindDN, bindCredential); // Validate the user by binding against the userDN bindDNAuthentication(ctx, username, credential, baseDN, baseFilter); } catch(Exception e) { throw e; } finally { if (ctx != null) ctx.close(); if (currentTCCL != null) SecurityActions.setContextClassLoader(currentTCCL); } return true; } @SuppressWarnings("rawtypes") private InitialLdapContext constructInitialLdapContext(String dn, Object credential) throws NamingException { Properties env = new Properties(); 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"); env.setProperty(Context.PROVIDER_URL, providerURL); // JBAS-3555, allow anonymous login with no bindDN and bindCredential if (dn != null) env.setProperty(Context.SECURITY_PRINCIPAL, dn); if (credential != null) env.put(Context.SECURITY_CREDENTIALS, credential); this.traceLDAPEnv(env); return new InitialLdapContext(env, null); } protected String bindDNAuthentication(InitialLdapContext ctx, String user, Object credential, String baseDN, String filter) throws NamingException { SearchControls constraints = new SearchControls(); constraints.setSearchScope(searchScope); constraints.setTimeLimit(searchTimeLimit); String attrList[] = {distinguishedNameAttribute}; constraints.setReturningAttributes(attrList); NamingEnumeration<SearchResult> results = null; Object[] filterArgs = {user}; results = ctx.search(baseDN, filter, filterArgs, constraints); if (!results.hasMore()) { results.close(); throw PicketBoxMessages.MESSAGES.failedToFindBaseContextDN(baseDN); } SearchResult sr = results.next(); String name = sr.getName(); String userDN = null; Attributes attrs = sr.getAttributes(); if (attrs != null) { Attribute dn = attrs.get(distinguishedNameAttribute); if (dn != null) { userDN = (String) dn.get(); } } if (userDN == null) { if (sr.isRelative()) userDN = name + ("".equals(baseDN) ? "" : "," + baseDN); else throw PicketBoxMessages.MESSAGES.unableToFollowReferralForAuth(name); } results.close(); results = null; // Bind as the user dn to authenticate the user InitialLdapContext userCtx = constructInitialLdapContext(userDN, credential); userCtx.close(); return userDN; } /** * <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, "******"); if (tmp.containsKey(BIND_CREDENTIAL)) tmp.setProperty(BIND_CREDENTIAL, "******"); PicketBoxLogger.LOGGER.traceLDAPConnectionEnv(tmp); } }