All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProviderCustom Maven / Gradle / Ivy

package org.springframework.security.ldap.authentication.ad;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.OperationNotSupportedException;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.Rdn;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.support.DefaultDirObjectFactory;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.ContextFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Specialized LDAP authentication provider which uses Active Directory configuration
 * conventions.
 * 

* It will authenticate using the Active Directory * * {@code userPrincipalName} or a custom {@link #setSearchFilter(String) searchFilter} * in the form {@code username@domain}. If the username does not already end with the * domain name, the {@code userPrincipalName} will be built by appending the configured * domain name to the username supplied in the authentication request. If no domain name * is configured, it is assumed that the username will always contain the domain name. *

* The user authorities are obtained from the data contained in the {@code memberOf} * attribute. * *

Active Directory Sub-Error Codes

* * When an authentication fails, resulting in a standard LDAP 49 error code, Active * Directory also supplies its own sub-error codes within the error message. These will be * used to provide additional log information on why an authentication has failed. Typical * examples are * *
    *
  • 525 - user not found
  • *
  • 52e - invalid credentials
  • *
  • 530 - not permitted to logon at this time
  • *
  • 532 - password expired
  • *
  • 533 - account disabled
  • *
  • 701 - account expired
  • *
  • 773 - user must reset password
  • *
  • 775 - account locked
  • *
* * If you set the {@link #setConvertSubErrorCodesToExceptions(boolean) * convertSubErrorCodesToExceptions} property to {@code true}, the codes will also be used * to control the exception raised. * * @author Luke Taylor * @author Rob Winch * @since 3.1 */ public class ActiveDirectoryLdapAuthenticationProviderCustom extends AbstractLdapAuthenticationProvider implements InitializingBean { private static final Pattern SUB_ERROR_CODE = Pattern.compile(".*data\\s([0-9a-f]{3,4}).*"); // Error codes private static final int USERNAME_NOT_FOUND = 0x525; private static final int INVALID_PASSWORD = 0x52e; private static final int NOT_PERMITTED = 0x530; private static final int PASSWORD_EXPIRED = 0x532; private static final int ACCOUNT_DISABLED = 0x533; private static final int ACCOUNT_EXPIRED = 0x701; private static final int PASSWORD_NEEDS_RESET = 0x773; private static final int ACCOUNT_LOCKED = 0x775; private String domain; private String rootDn; private String url; private boolean convertSubErrorCodesToExceptions; /** *
   * (&(objectClass=user)(userPrincipalName={0}))
   * 
*/ private String searchFilter = "(&(objectClass=user)(sAMAccountName={0}))"; private String readTimeout = "10000"; private String connectTimeout = "10000"; // Only used to allow tests to substitute a mock LdapContext ContextFactory contextFactory = new ContextFactory(); public ActiveDirectoryLdapAuthenticationProviderCustom() { super(); } /** * @param domain the domain name (may be null or empty) * @param url an LDAP url (or multiple URLs) * @param rootDn the root DN (may be null or empty) */ public ActiveDirectoryLdapAuthenticationProviderCustom(String domain, String url, String rootDn) { Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty"); this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null; this.url = url; this.rootDn = StringUtils.hasText(rootDn) ? rootDn.toLowerCase() : null; } /** * @param domain the domain name (may be null or empty) * @param url an LDAP url (or multiple URLs) */ public ActiveDirectoryLdapAuthenticationProviderCustom(String domain, String url) { Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty"); this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null; this.url = url; rootDn = this.domain == null ? null : rootDnFromDomain(this.domain); } @Override public DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken auth) { String username = auth.getName(); String password = (String) auth.getCredentials(); DirContext ctx = bindAsUser(username, password); try { return searchForUser(ctx, username); } catch (NamingException e) { if (!(e.getResolvedObj() instanceof Serializable)) { logger.error("Failed to locate directory entry for authenticated user: " + username, e); e.setResolvedObj(null); } throw badCredentials(e); } finally { LdapUtils.closeContext(ctx); } } /** * Creates the user authority list from the values of the {@code memberOf} attribute * obtained from the user's Active Directory entry. * * @see org.springframework.ldap.core.DistinguishedName#DistinguishedName(String) * @see org.springframework.ldap.core.DistinguishedName#removeLast() * @see org.springframework.ldap.core.LdapRdn#getValue() */ @Override protected Collection loadUserAuthorities(DirContextOperations userData, String username, String password) { String[] groups = userData.getStringAttributes("memberOf"); if (groups == null) { logger.debug("No values for 'memberOf' attribute."); return AuthorityUtils.NO_AUTHORITIES; } if (logger.isDebugEnabled()) { logger.debug("'memberOf' attribute values: " + Arrays.asList(groups)); } /** * ArrayList authorities = new * ArrayList(groups.length); * * for (String group : groups) { authorities.add(new SimpleGrantedAuthority(new * DistinguishedName(group).removeLast().getValue())); } * */ ArrayList authorities = new ArrayList(); for (String group : groups) { Iterator iterator = LdapUtils.newLdapName(group).getRdns().iterator(); Object value = null; while (iterator.hasNext()) { value = iterator.next().getValue(); } if (value instanceof String) { authorities.add(new SimpleGrantedAuthority((String) value)); } } String cn = userData.getStringAttribute("cn"); if (StringUtils.hasText(cn)) { authorities.add(new SimpleGrantedAuthority(cn)); } return authorities; } private DirContext bindAsUser(String username, String password) { // TODO. add DNS lookup based on domain final String bindUrl = url; Hashtable env = new Hashtable(); env.put(Context.SECURITY_AUTHENTICATION, "simple"); String bindPrincipal = createBindPrincipal(username); env.put(Context.SECURITY_PRINCIPAL, bindPrincipal); env.put(Context.PROVIDER_URL, bindUrl); env.put(Context.SECURITY_CREDENTIALS, password); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.OBJECT_FACTORIES, DefaultDirObjectFactory.class.getName()); env.put("com.sun.jndi.ldap.read.timeout", this.readTimeout); env.put("com.sun.jndi.ldap.connect.timeout", this.connectTimeout); try { return contextFactory.createContext(env); } catch (NamingException e) { if (!(e.getResolvedObj() instanceof Serializable)) { if (logger.isTraceEnabled()) { logger.trace("Active Directory authentication failed: " + e, e); } e.setResolvedObj(null); } if ((e instanceof AuthenticationException) || (e instanceof OperationNotSupportedException)) { handleBindException(bindPrincipal, e); throw badCredentials(e); } else { throw badCredentials(LdapUtils.convertLdapException(e)); } } } private void handleBindException(String bindPrincipal, NamingException exception) { if (logger.isDebugEnabled()) { logger.debug("Authentication for " + bindPrincipal + " failed:" + exception); } int subErrorCode = parseSubErrorCode(exception.getMessage()); if (subErrorCode <= 0) { logger.debug("Failed to locate AD-specific sub-error code in message"); return; } logger.info("Active Directory authentication failed: " + subCodeToLogMessage(subErrorCode)); if (convertSubErrorCodesToExceptions) { raiseExceptionForErrorCode(subErrorCode, exception); } } private int parseSubErrorCode(String message) { Matcher m = SUB_ERROR_CODE.matcher(message); if (m.matches()) { return Integer.parseInt(m.group(1), 16); } return -1; } private void raiseExceptionForErrorCode(int code, NamingException exception) { String hexString = Integer.toHexString(code); Throwable cause = new ActiveDirectoryAuthenticationException(hexString, exception.getMessage(), exception); switch (code) { case PASSWORD_EXPIRED: throw new CredentialsExpiredException(messages.getMessage("LdapAuthenticationProvider.credentialsExpired", "User credentials have expired"), cause); case ACCOUNT_DISABLED: throw new DisabledException(messages.getMessage("LdapAuthenticationProvider.disabled", "User is disabled"), cause); case ACCOUNT_EXPIRED: throw new AccountExpiredException(messages.getMessage("LdapAuthenticationProvider.expired", "User account has expired"), cause); case ACCOUNT_LOCKED: throw new LockedException(messages.getMessage("LdapAuthenticationProvider.locked", "User account is locked"), cause); default: throw badCredentials(cause); } } private String subCodeToLogMessage(int code) { switch (code) { case USERNAME_NOT_FOUND: return "User was not found in directory"; case INVALID_PASSWORD: return "Supplied password was invalid"; case NOT_PERMITTED: return "User not permitted to logon at this time"; case PASSWORD_EXPIRED: return "Password has expired"; case ACCOUNT_DISABLED: return "Account is disabled"; case ACCOUNT_EXPIRED: return "Account expired"; case PASSWORD_NEEDS_RESET: return "User must reset password"; case ACCOUNT_LOCKED: return "Account locked"; } return "Unknown (error code " + Integer.toHexString(code) + ")"; } private BadCredentialsException badCredentials() { return new BadCredentialsException(messages.getMessage("LdapAuthenticationProvider.badCredentials", "Bad credentials")); } private BadCredentialsException badCredentials(Throwable cause) { // if (cause instanceof ActiveDirectoryAuthenticationException && cause.getCause() // instanceof NamingException) { // if (!(((NamingException) cause.getCause()).getResolvedObj() instanceof // Serializable)) { // ((NamingException) cause.getCause()).setResolvedObj(null); // } // } return (BadCredentialsException) badCredentials().initCause(cause); } /** * String bindPrincipal = createBindPrincipal(username); */ private DirContextOperations searchForUser(DirContext context, String username) throws NamingException { SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); String bindPrincipal = username; String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal); try { return SpringSecurityLdapTemplate.searchForSingleEntryInternal(context, searchControls, searchRoot, searchFilter, new Object[] { bindPrincipal }); } catch (IncorrectResultSizeDataAccessException incorrectResults) { // Search should never return multiple results if properly configured - just // rethrow if (incorrectResults.getActualSize() != 0) { throw incorrectResults; } // If we found no results, then the username/password did not match UsernameNotFoundException userNameNotFoundException = new UsernameNotFoundException("User " + username + " not found in directory.", incorrectResults); throw badCredentials(userNameNotFoundException); } } private String searchRootFromPrincipal(String bindPrincipal) { int atChar = bindPrincipal.lastIndexOf('@'); if (atChar < 0) { logger.debug("User principal '" + bindPrincipal + "' does not contain the domain, and no domain has been configured"); throw badCredentials(); } return rootDnFromDomain(bindPrincipal.substring(atChar + 1, bindPrincipal.length())); } private String rootDnFromDomain(String domain) { String[] tokens = StringUtils.tokenizeToStringArray(domain, "."); StringBuilder root = new StringBuilder(); for (String token : tokens) { if (root.length() > 0) { root.append(','); } root.append("dc=").append(token); } return root.toString(); } String createBindPrincipal(String username) { if (domain == null || username.toLowerCase().endsWith(domain)) { return username; } return username + "@" + domain; } /** * By default, a failed authentication (LDAP error 49) will result in a * {@code BadCredentialsException}. *

* If this property is set to {@code true}, the exception message from a failed bind * attempt will be parsed for the AD-specific error code and a * {@link CredentialsExpiredException}, {@link DisabledException}, * {@link AccountExpiredException} or {@link LockedException} will be thrown for the * corresponding codes. All other codes will result in the default * {@code BadCredentialsException}. * * @param convertSubErrorCodesToExceptions {@code true} to raise an exception based on * the AD error code. */ public void setConvertSubErrorCodesToExceptions(boolean convertSubErrorCodesToExceptions) { this.convertSubErrorCodesToExceptions = convertSubErrorCodesToExceptions; } /** * The LDAP filter string to search for the user being authenticated. Occurrences of {0} * are replaced with the {@code username@domain}. *

* Defaults to: {@code (&(objectClass=user)(userPrincipalName= 0}))} *

* * @param searchFilter the filter string * * @since 3.2.6 */ public void setSearchFilter(String searchFilter) { Assert.hasText(searchFilter, "searchFilter must have text"); this.searchFilter = searchFilter; } public void setReadTimeout(int readTimeout) { this.readTimeout = String.valueOf(readTimeout); } public void setConnectTimeout(int connectTimeout) { this.connectTimeout = String.valueOf(connectTimeout); } public String getDomain() { return domain; } public void setDomain(String domain) { this.domain = domain; } public String getRootDn() { return rootDn; } public void setRootDn(String rootDn) { this.rootDn = rootDn; } public String getUrl() { return url; } public void setUrl(String url) { Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty"); this.url = url; } @Override public void afterPropertiesSet() throws Exception { Assert.isTrue(StringUtils.hasText(this.url), "Url cannot be empty"); this.domain = StringUtils.hasText(this.domain) ? this.domain.toLowerCase() : null; this.rootDn = StringUtils.hasText(this.rootDn) ? this.rootDn.toLowerCase() : (this.domain == null ? null : rootDnFromDomain(this.domain)); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy