| // |
| // ======================================================================== |
| // Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.jaas.spi; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Hashtable; |
| import java.util.List; |
| import java.util.Locale; |
| 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.eclipse.jetty.jaas.callback.ObjectCallback; |
| import org.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| import org.eclipse.jetty.util.security.Credential; |
| |
| /** |
| * A LdapLoginModule for use with JAAS setups |
| * <p> |
| * The jvm should be started with the following parameter: |
| * <pre> |
| * -Djava.security.auth.login.config=etc/ldap-loginModule.conf |
| * </pre> |
| * and an example of the ldap-loginModule.conf would be: |
| * <pre> |
| * ldaploginmodule { |
| * org.eclipse.jetty.server.server.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"; |
| * }; |
| * </pre> |
| */ |
| public class LdapLoginModule extends AbstractLoginModule |
| { |
| private static final Logger LOG = Log.getLogger(LdapLoginModule.class); |
| |
| /** |
| * 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 |
| * <p> |
| * 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; |
| |
| |
| public class LDAPUserInfo extends UserInfo |
| { |
| |
| /** |
| * @param userName |
| * @param credential |
| */ |
| public LDAPUserInfo(String userName, Credential credential) |
| { |
| super(userName, credential); |
| } |
| |
| @Override |
| public List<String> doFetchRoles() throws Exception |
| { |
| return getUserRoles(_rootContext, getUserName()); |
| } |
| |
| } |
| |
| |
| /** |
| * get the available information about the user |
| * <p> |
| * for this LoginModule, the credential can be null which will result in a |
| * binding ldap authentication scenario |
| * <p> |
| * roles are also an optional concept if required |
| * |
| * @param username the user name |
| * @return the userinfo for the username |
| * @throws Exception if unable to get the user info |
| */ |
| public UserInfo getUserInfo(String username) throws Exception |
| { |
| String pwdCredential = getUserCredentials(username); |
| |
| if (pwdCredential == null) |
| { |
| return null; |
| } |
| |
| pwdCredential = convertCredentialLdapToJetty(pwdCredential); |
| Credential credential = Credential.getCredential(pwdCredential); |
| return new LDAPUserInfo(username, credential); |
| } |
| |
| 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 |
| * <p> |
| * 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<SearchResult> 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 |
| * <p> |
| * NOTE: this is not an user authenticated operation |
| * |
| * @param dirContext |
| * @param username |
| * @return |
| * @throws LoginException |
| */ |
| private List<String> getUserRoles(DirContext dirContext, String username) throws LoginException, NamingException |
| { |
| String userDn = _userRdnAttribute + "=" + username + "," + _userBaseDn; |
| |
| return getUserRolesByDn(dirContext, userDn); |
| } |
| |
| private List<String> getUserRolesByDn(DirContext dirContext, String userDn) throws LoginException, NamingException |
| { |
| List<String> roleList = new ArrayList<String>(); |
| |
| if (dirContext == null || _roleBaseDn == null || _roleMemberAttribute == null || _roleObjectClass == null) |
| { |
| return roleList; |
| } |
| |
| SearchControls ctls = new SearchControls(); |
| ctls.setDerefLinkFlag(true); |
| ctls.setSearchScope(SearchControls.SUBTREE_SCOPE); |
| ctls.setReturningAttributes(new String[]{_roleNameAttribute}); |
| |
| String filter = "(&(objectClass={0})({1}={2}))"; |
| Object[] filterArguments = {_roleObjectClass, _roleMemberAttribute, userDn}; |
| NamingEnumeration<SearchResult> 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().toString()); |
| } |
| } |
| |
| return roleList; |
| } |
| |
| |
| /** |
| * since ldap uses a context bind for valid authentication checking, we override login() |
| * <p> |
| * 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 true if authenticated, false otherwise |
| * @throws LoginException if unable to login |
| */ |
| 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)); |
| |
| boolean authed = false; |
| if (webCredential instanceof String) |
| authed = credentialLogin(Credential.getCredential((String) webCredential)); |
| else |
| authed = credentialLogin(webCredential); |
| |
| //only fetch roles if authenticated |
| if (authed) |
| getCurrentUser().fetchRoles(); |
| |
| return authed; |
| } |
| 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 the web credential |
| * @return true if authenticated |
| * @throws LoginException if unable to login |
| */ |
| protected boolean credentialLogin(Object webCredential) throws LoginException |
| { |
| setAuthenticated(getCurrentUser().checkCredential(webCredential)); |
| return isAuthenticated(); |
| } |
| |
| /** |
| * binding authentication check |
| * This method of authentication works only if the user branch of the DIT (ldap tree) |
| * has an ACI (access control instruction) that allow the access to any user or at least |
| * for the user that logs in. |
| * |
| * @param username the user name |
| * @param password the password |
| * @return true always |
| * @throws LoginException if unable to bind the login |
| * @throws NamingException if failure to bind login |
| */ |
| public boolean bindingLogin(String username, Object password) throws LoginException, NamingException |
| { |
| SearchResult searchResult = findUser(username); |
| |
| String userDn = searchResult.getNameInNamespace(); |
| |
| LOG.info("Attempting authentication: " + userDn); |
| |
| Hashtable<Object,Object> environment = getEnvironment(); |
| environment.put(Context.SECURITY_PRINCIPAL, userDn); |
| environment.put(Context.SECURITY_CREDENTIALS, password); |
| |
| DirContext dirContext = new InitialDirContext(environment); |
| List<String> 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}))"; |
| |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Searching for users with filter: \'" + filter + "\'" + " from base dn: " + _userBaseDn); |
| |
| Object[] filterArguments = new Object[]{ |
| _userObjectClass, |
| _userIdAttribute, |
| username |
| }; |
| NamingEnumeration<SearchResult> results = _rootContext.search(_userBaseDn, filter, filterArguments, ctls); |
| |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Found user?: " + results.hasMoreElements()); |
| |
| if (!results.hasMoreElements()) |
| { |
| throw new LoginException("User not found."); |
| } |
| |
| return (SearchResult) results.nextElement(); |
| } |
| |
| |
| /** |
| * Init LoginModule. |
| * <p> |
| * Called once by JAAS after new instance is created. |
| * |
| * @param subject the subect |
| * @param callbackHandler the callback handler |
| * @param sharedState the shared state map |
| * @param options the option map |
| */ |
| public void initialize(Subject subject, |
| CallbackHandler callbackHandler, |
| Map<String,?> sharedState, |
| Map<String,?> 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.parseBoolean((String) options.get("forceBindingLogin")); |
| } |
| |
| 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.parseBoolean(String.valueOf(getOption(options, "debug", Boolean.toString(_debug)))); |
| |
| try |
| { |
| _rootContext = new InitialDirContext(getEnvironment()); |
| } |
| catch (NamingException ex) |
| { |
| throw new IllegalStateException("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<String,?> options, String key, String defaultValue) |
| { |
| Object value = options.get(key); |
| |
| if (value == null) |
| { |
| return defaultValue; |
| } |
| |
| return (String) value; |
| } |
| |
| /** |
| * get the context for connection |
| * |
| * @return the environment details for the context |
| */ |
| public Hashtable<Object, Object> 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(Locale.ENGLISH))) |
| { |
| return "{MD5}" + encryptedPassword.substring("MD5:".length(), encryptedPassword.length()); |
| } |
| |
| if ("CRYPT:".startsWith(encryptedPassword.toUpperCase(Locale.ENGLISH))) |
| { |
| 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(Locale.ENGLISH))) |
| { |
| return "MD5:" + encryptedPassword.substring("{MD5}".length(), encryptedPassword.length()); |
| } |
| |
| if ("{CRYPT}".startsWith(encryptedPassword.toUpperCase(Locale.ENGLISH))) |
| { |
| return "CRYPT:" + encryptedPassword.substring("{CRYPT}".length(), encryptedPassword.length()); |
| } |
| |
| return encryptedPassword; |
| } |
| } |