package org.mortbay.jetty.plus.jaas.ldap; // ======================================================================== // Copyright 2007 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // 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. // ======================================================================== import java.io.IOException; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import java.util.Map; 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.DirContext; import javax.naming.directory.InitialDirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.login.LoginException; import org.mortbay.jetty.plus.jaas.callback.ObjectCallback; import org.mortbay.jetty.plus.jaas.spi.AbstractLoginModule; import org.mortbay.jetty.plus.jaas.spi.UserInfo; import org.mortbay.jetty.security.Credential; import org.mortbay.log.Log; /** * * A LdapLoginModule for use with JAAS setups * * The jvm should be started with the following parameter: *

* * -Djava.security.auth.login.config=etc/ldap-loginModule.conf * *

* and an example of the ldap-loginModule.conf would be: *

*
 * ldaploginmodule {
 *    org.mortbay.jetty.plus.jaas.spi.LdapLoginModule required
 *    debug="true"
 *    useLdaps="false"
 *    contextFactory="com.sun.jndi.ldap.LdapCtxFactory"
 *    hostname="ldap.example.com"
 *    port="389"
 *    bindDn="cn=Directory Manager"
 *    bindPassword="directory"
 *    authenticationMethod="simple"
 *    forceBindingLogin="false"
 *    userBaseDn="ou=people,dc=alcatel"
 *    userRdnAttribute="uid"
 *    userIdAttribute="uid"
 *    userPasswordAttribute="userPassword"
 *    userObjectClass="inetOrgPerson"
 *    roleBaseDn="ou=groups,dc=example,dc=com"
 *    roleNameAttribute="cn"
 *    roleMemberAttribute="uniqueMember"
 *    roleObjectClass="groupOfUniqueNames";
 *    };
 *  
* * @author Jesse McConnell * @author Frederic Nizery * @author Trygve Laugstol */ public class LdapLoginModule extends AbstractLoginModule { /** * hostname of the ldap server */ private String _hostname; /** * port of the ldap server */ private int _port; /** * Context.SECURITY_AUTHENTICATION */ private String _authenticationMethod; /** * Context.INITIAL_CONTEXT_FACTORY */ private String _contextFactory; /** * root DN used to connect to */ private String _bindDn; /** * password used to connect to the root ldap context */ private String _bindPassword; /** * object class of a user */ private String _userObjectClass = "inetOrgPerson"; /** * attribute that the principal is located */ private String _userRdnAttribute = "uid"; /** * attribute that the principal is located */ private String _userIdAttribute = "cn"; /** * name of the attribute that a users password is stored under *

* NOTE: not always accessible, see force binding login */ private String _userPasswordAttribute = "userPassword"; /** * base DN where users are to be searched from */ private String _userBaseDn; /** * base DN where role membership is to be searched from */ private String _roleBaseDn; /** * object class of roles */ private String _roleObjectClass = "groupOfUniqueNames"; /** * name of the attribute that a username would be under a role class */ private String _roleMemberAttribute = "uniqueMember"; /** * the name of the attribute that a role would be stored under */ private String _roleNameAttribute = "roleName"; private boolean _debug; /** * if the getUserInfo can pull a password off of the user then * password comparison is an option for authn, to force binding * login checks, set this to true */ private boolean _forceBindingLogin = false; /** * When true changes the protocol to ldaps */ private boolean _useLdaps = false; private DirContext _rootContext; /** * get the available information about the user *

* for this LoginModule, the credential can be null which will result in a * binding ldap authentication scenario *

* roles are also an optional concept if required * * @param username * @return * @throws Exception */ public UserInfo getUserInfo(String username) throws Exception { String pwdCredential = getUserCredentials(username); if (pwdCredential == null) { return null; } pwdCredential = convertCredentialLdapToJetty(pwdCredential); Credential credential = Credential.getCredential(pwdCredential); List roles = getUserRoles(_rootContext, username); return new UserInfo(username, credential, roles); } protected String doRFC2254Encoding(String inputString) { StringBuffer buf = new StringBuffer(inputString.length()); for (int i = 0; i < inputString.length(); i++) { char c = inputString.charAt(i); switch (c) { case '\\': buf.append("\\5c"); break; case '*': buf.append("\\2a"); break; case '(': buf.append("\\28"); break; case ')': buf.append("\\29"); break; case '\0': buf.append("\\00"); break; default: buf.append(c); break; } } return buf.toString(); } /** * attempts to get the users credentials from the users context *

* NOTE: this is not an user authenticated operation * * @param username * @return * @throws LoginException */ private String getUserCredentials(String username) throws LoginException { String ldapCredential = null; SearchControls ctls = new SearchControls(); ctls.setCountLimit(1); ctls.setDerefLinkFlag(true); ctls.setSearchScope(SearchControls.SUBTREE_SCOPE); String filter = "(&(objectClass={0})({1}={2}))"; Log.debug("Searching for users with filter: \'" + filter + "\'" + " from base dn: " + _userBaseDn); try { Object[] filterArguments = {_userObjectClass, _userIdAttribute, username}; NamingEnumeration results = _rootContext.search(_userBaseDn, filter, filterArguments, ctls); Log.debug("Found user?: " + results.hasMoreElements()); if (!results.hasMoreElements()) { throw new LoginException("User not found."); } SearchResult result = findUser(username); Attributes attributes = result.getAttributes(); Attribute attribute = attributes.get(_userPasswordAttribute); if (attribute != null) { try { byte[] value = (byte[]) attribute.get(); ldapCredential = new String(value); } catch (NamingException e) { Log.debug("no password available under attribute: " + _userPasswordAttribute); } } } catch (NamingException e) { throw new LoginException("Root context binding failure."); } Log.debug("user cred is: " + ldapCredential); return ldapCredential; } /** * attempts to get the users roles from the root context *

* NOTE: this is not an user authenticated operation * * @param dirContext * @param username * @return * @throws LoginException */ private List getUserRoles(DirContext dirContext, String username) throws LoginException, NamingException { String userDn = _userRdnAttribute + "=" + username + "," + _userBaseDn; return getUserRolesByDn(dirContext, userDn); } private List getUserRolesByDn(DirContext dirContext, String userDn) throws LoginException, NamingException { ArrayList roleList = new ArrayList(); if (dirContext == null || _roleBaseDn == null || _roleMemberAttribute == null || _roleObjectClass == null) { return roleList; } SearchControls ctls = new SearchControls(); ctls.setDerefLinkFlag(true); ctls.setSearchScope(SearchControls.SUBTREE_SCOPE); String filter = "(&(objectClass={0})({1}={2}))"; Object[] filterArguments = {_roleObjectClass, _roleMemberAttribute, userDn}; NamingEnumeration results = dirContext.search(_roleBaseDn, filter, filterArguments, ctls); Log.debug("Found user roles?: " + results.hasMoreElements()); while (results.hasMoreElements()) { SearchResult result = (SearchResult)results.nextElement(); Attributes attributes = result.getAttributes(); if (attributes == null) { continue; } Attribute roleAttribute = attributes.get(_roleNameAttribute); if (roleAttribute == null) { continue; } NamingEnumeration roles = roleAttribute.getAll(); while (roles.hasMore()) { roleList.add(roles.next()); } } return roleList; } /** * since ldap uses a context bind for valid authentication checking, we override login() *

* if credentials are not available from the users context or if we are forcing the binding check * then we try a binding authentication check, otherwise if we have the users encoded password then * we can try authentication via that mechanic * * @return * @throws LoginException */ public boolean login() throws LoginException { try { if (getCallbackHandler() == null) { throw new LoginException("No callback handler"); } Callback[] callbacks = configureCallbacks(); getCallbackHandler().handle(callbacks); String webUserName = ((NameCallback) callbacks[0]).getName(); Object webCredential = ((ObjectCallback) callbacks[1]).getObject(); if (webUserName == null || webCredential == null) { setAuthenticated(false); return isAuthenticated(); } if (_forceBindingLogin) { return bindingLogin(webUserName, webCredential); } // This sets read and the credential UserInfo userInfo = getUserInfo(webUserName); if( userInfo == null) { setAuthenticated(false); return false; } setCurrentUser(new JAASUserInfo(userInfo)); if (webCredential instanceof String) { return credentialLogin(Credential.getCredential((String) webCredential)); } return credentialLogin(webCredential); } catch (UnsupportedCallbackException e) { throw new LoginException("Error obtaining callback information."); } catch (IOException e) { if (_debug) { e.printStackTrace(); } throw new LoginException("IO Error performing login."); } catch (Exception e) { if (_debug) { e.printStackTrace(); } throw new LoginException("Error obtaining user info."); } } /** * password supplied authentication check * * @param webCredential * @return * @throws LoginException */ protected boolean credentialLogin(Object webCredential) throws LoginException { setAuthenticated(getCurrentUser().checkCredential(webCredential)); return isAuthenticated(); } /** * binding authentication check * This methode of authentication works only if the user branch of the DIT (ldap tree) * has an ACI (acces control instruction) that allow the access to any user or at least * for the user that logs in. * * @param username * @param password * @return * @throws LoginException */ protected boolean bindingLogin(String username, Object password) throws LoginException, NamingException { SearchResult searchResult = findUser(username); DirContext usrsContext = (DirContext)_rootContext.lookup(_userBaseDn); DirContext usrContext = (DirContext)usrsContext.lookup(searchResult.getName()); String userDn = usrContext.getNameInNamespace(); Log.info("Attempting authentication: " + userDn); Hashtable environment = getEnvironment(); environment.put(Context.SECURITY_PRINCIPAL, userDn); environment.put(Context.SECURITY_CREDENTIALS, password); DirContext dirContext = new InitialDirContext(environment); List roles = getUserRolesByDn(dirContext, userDn); UserInfo userInfo = new UserInfo(username, null, roles); setCurrentUser(new JAASUserInfo(userInfo)); setAuthenticated(true); return true; } private SearchResult findUser(String username) throws NamingException, LoginException { SearchControls ctls = new SearchControls(); ctls.setCountLimit(1); ctls.setDerefLinkFlag(true); ctls.setSearchScope(SearchControls.SUBTREE_SCOPE); String filter = "(&(objectClass={0})({1}={2}))"; Log.info("Searching for users with filter: \'" + filter + "\'" + " from base dn: " + _userBaseDn); Object[] filterArguments = new Object[]{ _userObjectClass, _userIdAttribute, username }; NamingEnumeration results = _rootContext.search(_userBaseDn, filter, filterArguments, ctls); Log.info("Found user?: " + results.hasMoreElements()); if (!results.hasMoreElements()) { throw new LoginException("User not found."); } return (SearchResult)results.nextElement(); } public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { super.initialize(subject, callbackHandler, sharedState, options); _hostname = (String) options.get("hostname"); _port = Integer.parseInt((String) options.get("port")); _contextFactory = (String) options.get("contextFactory"); _bindDn = (String) options.get("bindDn"); _bindPassword = (String) options.get("bindPassword"); _authenticationMethod = (String) options.get("authenticationMethod"); _userBaseDn = (String) options.get("userBaseDn"); _roleBaseDn = (String) options.get("roleBaseDn"); if (options.containsKey("forceBindingLogin")) { _forceBindingLogin = Boolean.valueOf((String) options.get("forceBindingLogin")).booleanValue(); } if (options.containsKey("useLdaps")) { _useLdaps = Boolean.parseBoolean((String) options.get("useLdaps")); } _userObjectClass = getOption(options, "userObjectClass", _userObjectClass); _userRdnAttribute = getOption(options, "userRdnAttribute", _userRdnAttribute); _userIdAttribute = getOption(options, "userIdAttribute", _userIdAttribute); _userPasswordAttribute = getOption(options, "userPasswordAttribute", _userPasswordAttribute); _roleObjectClass = getOption(options, "roleObjectClass", _roleObjectClass); _roleMemberAttribute = getOption(options, "roleMemberAttribute", _roleMemberAttribute); _roleNameAttribute = getOption(options, "roleNameAttribute", _roleNameAttribute); _debug = Boolean.valueOf(String.valueOf(getOption(options, "debug", Boolean.toString(_debug)))).booleanValue(); try { _rootContext = new InitialDirContext(getEnvironment()); } catch (NamingException ex) { throw new RuntimeException("Unable to establish root context", ex); } } public boolean commit() throws LoginException { try { _rootContext.close(); } catch (NamingException e) { throw new LoginException("error closing root context: " + e.getMessage()); } return super.commit(); } public boolean abort() throws LoginException { try { _rootContext.close(); } catch (NamingException e) { throw new LoginException("error closing root context: " + e.getMessage()); } return super.abort(); } private String getOption(Map options, String key, String defaultValue) { Object value = options.get(key); if (value == null) { return defaultValue; } return (String) value; } /** * get the context for connection * * @return */ public Hashtable getEnvironment() { Properties env = new Properties(); env.put(Context.INITIAL_CONTEXT_FACTORY, _contextFactory); if (_hostname != null) { env.put(Context.PROVIDER_URL, (_useLdaps?"ldaps://":"ldap://") + _hostname + (_port==0?"":":"+_port) +"/"); } if (_authenticationMethod != null) { env.put(Context.SECURITY_AUTHENTICATION, _authenticationMethod); } if (_bindDn != null) { env.put(Context.SECURITY_PRINCIPAL, _bindDn); } if (_bindPassword != null) { env.put(Context.SECURITY_CREDENTIALS, _bindPassword); } return env; } public static String convertCredentialJettyToLdap( String encryptedPassword ) { if ("MD5:".startsWith(encryptedPassword.toUpperCase())) { return "{MD5}" + encryptedPassword.substring("MD5:".length(), encryptedPassword.length()); } if ("CRYPT:".startsWith(encryptedPassword.toUpperCase())) { return "{CRYPT}" + encryptedPassword.substring("CRYPT:".length(), encryptedPassword.length()); } return encryptedPassword; } public static String convertCredentialLdapToJetty( String encryptedPassword ) { if (encryptedPassword == null) { return encryptedPassword; } if ("{MD5}".startsWith(encryptedPassword.toUpperCase())) { return "MD5:" + encryptedPassword.substring("{MD5}".length(), encryptedPassword.length()); } if ("{CRYPT}".startsWith(encryptedPassword.toUpperCase())) { return "CRYPT:" + encryptedPassword.substring("{CRYPT}".length(), encryptedPassword.length()); } return encryptedPassword; } }