From 7a9d59f64f7500f5c98bf1249da7b1fbe1251e27 Mon Sep 17 00:00:00 2001 From: Dieter Hubau Date: Thu, 17 Dec 2015 15:22:14 +0100 Subject: [PATCH] AD-bind-principal: New abstraction for ActiveDirectory LDAP auth Most use cases don't require another implementation than the standard way of binding and searching for a user in Active Directory. However, sometimes there is a different implementation needed for creating the principal string for binding a user and the principal string for searching a user. The new abstract class allows for users to create a different implementation according to their needs, based on the given authentication token (or a subclass of the authentication token). --- ...veDirectoryLdapAuthenticationProvider.java | 382 ++++++++++++++++ ...veDirectoryLdapAuthenticationProvider.java | 415 ++---------------- 2 files changed, 427 insertions(+), 370 deletions(-) create mode 100644 ldap/src/main/java/org/springframework/security/ldap/authentication/ad/AbstractActiveDirectoryLdapAuthenticationProvider.java diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/AbstractActiveDirectoryLdapAuthenticationProvider.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/AbstractActiveDirectoryLdapAuthenticationProvider.java new file mode 100644 index 00000000000..96aa1d9eb9a --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/AbstractActiveDirectoryLdapAuthenticationProvider.java @@ -0,0 +1,382 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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 org.springframework.security.ldap.authentication.ad; + +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.ldap.core.support.DefaultDirObjectFactory; +import org.springframework.ldap.support.LdapUtils; +import org.springframework.security.authentication.*; +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.util.Assert; +import org.springframework.util.StringUtils; + +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.InitialLdapContext; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Hashtable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Abstract LDAP authentication provider which uses Active Directory configuration + * conventions. + *

+ * 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 + *

+ *

+ *

+ * If you set the {@link #setConvertSubErrorCodesToExceptions(boolean) + * convertSubErrorCodesToExceptions} property to {@code true}, the codes will also be used + * to control the exception raised. + *

+ * Usually, you would only need the standard implementation of {@link ActiveDirectoryLdapAuthenticationProvider} + * but you can implement the + * {@link #createBindPrincipal(UsernamePasswordAuthenticationToken)} and the + * {@link #createSearchPrincipal(UsernamePasswordAuthenticationToken)} + * methods yourself to modify the standard behaviour. + * + * @author Luke Taylor + * @author Rob Winch + * @author Dieter Hubau + * @see ActiveDirectoryLdapAuthenticationProvider for the standard implementation + * @since 4.1 + */ +public abstract class AbstractActiveDirectoryLdapAuthenticationProvider extends AbstractLdapAuthenticationProvider { + + protected static final Pattern SUB_ERROR_CODE = Pattern.compile(".*data\\s([0-9a-f]{3,4}).*"); + + // Error codes + protected static final int USERNAME_NOT_FOUND = 0x525; + protected static final int INVALID_PASSWORD = 0x52e; + protected static final int NOT_PERMITTED = 0x530; + protected static final int PASSWORD_EXPIRED = 0x532; + protected static final int ACCOUNT_DISABLED = 0x533; + protected static final int ACCOUNT_EXPIRED = 0x701; + protected static final int PASSWORD_NEEDS_RESET = 0x773; + protected static final int ACCOUNT_LOCKED = 0x775; + + protected final String domain; + protected final String rootDn; + protected final String url; + + protected boolean convertSubErrorCodesToExceptions; + protected String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))"; + + // Only used to allow tests to substitute a mock LdapContext + ContextFactory contextFactory = new ContextFactory(); + + protected AbstractActiveDirectoryLdapAuthenticationProvider(final String domain, final String url, final 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; + } + + protected AbstractActiveDirectoryLdapAuthenticationProvider(final String domain, final 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 + protected DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken auth) { + DirContext ctx = bindAsUser(auth); + + try { + return searchForUser(ctx, auth); + } catch (NamingException e) { + logger.error("Failed to locate directory entry for authenticated user: " + auth.getName(), e); + throw badCredentials(e); + } finally { + LdapUtils.closeContext(ctx); + } + } + + protected DirContextOperations searchForUser(final DirContext context, final UsernamePasswordAuthenticationToken auth) throws NamingException { + String username = auth.getName(); + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + + String bindPrincipal = createBindPrincipal(auth); + String searchPrincipal = createSearchPrincipal(auth); + + String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal); + + try { + return SpringSecurityLdapTemplate.searchForSingleEntryInternal(context, + searchControls, searchRoot, searchFilter, + new Object[]{searchPrincipal}); + } 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 DirContext bindAsUser(final UsernamePasswordAuthenticationToken auth) { + // TODO. add DNS lookup based on domain + String password = (String) auth.getCredentials(); + + Hashtable env = new Hashtable(); + env.put(Context.SECURITY_AUTHENTICATION, "simple"); + String bindPrincipal = createBindPrincipal(auth); + env.put(Context.SECURITY_PRINCIPAL, bindPrincipal); + env.put(Context.PROVIDER_URL, url); + 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()); + + try { + return contextFactory.createContext(env); + } catch (NamingException e) { + if ((e instanceof AuthenticationException) || (e instanceof OperationNotSupportedException)) { + handleBindException(bindPrincipal, e); + throw badCredentials(e); + } else { + throw LdapUtils.convertLdapException(e); + } + } + } + + /** + * Creates the principal string used to bind to the LDAP context. + * + * @param auth the authentication token + * @return the principal string used to bind with to the LDAP context + */ + protected abstract String createBindPrincipal(UsernamePasswordAuthenticationToken auth); + + /** + * Creates the principal string used to search with in LDAP. + * + * @param auth the authentication token + * @return the principal string used to search with in LDAP. + */ + protected abstract String createSearchPrincipal(UsernamePasswordAuthenticationToken auth); + + /** + * Creates the user authority list from the values of the {@code memberOf} attribute + * obtained from the user's Active Directory entry. + */ + @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())); + } + + return authorities; + } + + 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(); + } + + 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) { + return (BadCredentialsException) badCredentials().initCause(cause); + } + + /** + * 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; + } + + static class ContextFactory { + DirContext createContext(Hashtable env) throws NamingException { + return new InitialLdapContext(env, null); + } + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java index 1d74992b289..5d01336b65d 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java @@ -12,41 +12,12 @@ */ package org.springframework.security.ldap.authentication.ad; -import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.ldap.core.DirContextOperations; -import org.springframework.ldap.core.DistinguishedName; -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.util.Assert; -import org.springframework.util.StringUtils; - -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.InitialLdapContext; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * 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} @@ -54,349 +25,53 @@ * 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 - * - * - * - * 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 + * @author Dieter Hubau * @since 3.1 */ -public final class ActiveDirectoryLdapAuthenticationProvider extends - AbstractLdapAuthenticationProvider { - 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 final String domain; - private final String rootDn; - private final String url; - private boolean convertSubErrorCodesToExceptions; - private String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))"; - - // Only used to allow tests to substitute a mock LdapContext - ContextFactory contextFactory = new ContextFactory(); - - /** - * @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 ActiveDirectoryLdapAuthenticationProvider(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 ActiveDirectoryLdapAuthenticationProvider(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 - protected 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) { - logger.error("Failed to locate directory entry for authenticated user: " - + username, e); - 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. - */ - @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())); - } - - 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()); - - try { - return contextFactory.createContext(env); - } - catch (NamingException e) { - if ((e instanceof AuthenticationException) - || (e instanceof OperationNotSupportedException)) { - handleBindException(bindPrincipal, e); - throw badCredentials(e); - } - else { - throw 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) { - return (BadCredentialsException) badCredentials().initCause(cause); - } - - private DirContextOperations searchForUser(DirContext context, String username) - throws NamingException { - SearchControls searchControls = new SearchControls(); - searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); - - String bindPrincipal = createBindPrincipal(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; - } - - static class ContextFactory { - DirContext createContext(Hashtable env) throws NamingException { - return new InitialLdapContext(env, null); - } - } +public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractActiveDirectoryLdapAuthenticationProvider { + + /** + * @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 ActiveDirectoryLdapAuthenticationProvider(String domain, String url, String rootDn) { + super(domain, url, rootDn); + } + + /** + * @param domain the domain name (may be null or empty) + * @param url an LDAP url (or multiple URLs) + */ + public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) { + super(domain, url); + } + + @Override + protected String createBindPrincipal(final UsernamePasswordAuthenticationToken auth) { + String username = auth.getName(); + return createBindPrincipal(username); + } + + @Override + protected String createSearchPrincipal(final UsernamePasswordAuthenticationToken auth) { + return createBindPrincipal(auth); + } + + /** + * Creates a bind principal string from a given username and the optional domain. + * + * @param username username + * @return bind string containing username and domain + */ + public String createBindPrincipal(String username) { + if (domain == null || username.toLowerCase().endsWith(domain)) { + return username; + } + + return username + "@" + domain; + } }