SEC-1915: Custom ActiveDirectory search filter

Currently the search filter used when retrieving user details is hard coded.

New property in ActiveDirectoryLdapAuthenticationProvider:
- searchFilter - the LDAP search filter to use when searching for authorities,
default to search using 'userPrincipalName' (current) OR 'sAMAccountName'
This commit is contained in:
Mateusz Rasiński 2015-01-11 00:16:52 +01:00 committed by Rob Winch
parent 5f57e5b0c3
commit c54346b690
2 changed files with 79 additions and 59 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2012 the original author or authors. * 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 * 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 * the License. You may obtain a copy of the License at
@ -47,11 +47,12 @@ import java.util.regex.Pattern;
* Specialized LDAP authentication provider which uses Active Directory configuration conventions. * Specialized LDAP authentication provider which uses Active Directory configuration conventions.
* <p> * <p>
* It will authenticate using the Active Directory * It will authenticate using the Active Directory
* <a href="http://msdn.microsoft.com/en-us/library/ms680857%28VS.85%29.aspx">{@code userPrincipalName}</a> * <a href="http://msdn.microsoft.com/en-us/library/ms680857%28VS.85%29.aspx">{@code userPrincipalName}</a> or
* (in the form {@code username@domain}). If the username does not already end with the domain name, the * <a href="http://msdn.microsoft.com/en-us/library/ms679635%28v=vs.85%29.aspx">{@code sAMAccountName}</a> (or a custom
* {@code userPrincipalName} will be built by appending the configured domain name to the username supplied in the * {@link #setSearchFilter(String) searchFilter}) in the form {@code username@domain}. If the username does not
* authentication request. If no domain name is configured, it is assumed that the username will always contain the * already end with the domain name, the {@code userPrincipalName} will be built by appending the configured domain
* domain name. * 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.
* <p> * <p>
* The user authorities are obtained from the data contained in the {@code memberOf} attribute. * The user authorities are obtained from the data contained in the {@code memberOf} attribute.
* *
@ -96,17 +97,11 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
private final String rootDn; private final String rootDn;
private final String url; private final String url;
private boolean convertSubErrorCodesToExceptions; private boolean convertSubErrorCodesToExceptions;
private String searchFilter = "(&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={0})))";
// Only used to allow tests to substitute a mock LdapContext // Only used to allow tests to substitute a mock LdapContext
ContextFactory contextFactory = new ContextFactory(); ContextFactory contextFactory = new ContextFactory();
/**
* @param domain the domain for which authentication should take place
*/
// public ActiveDirectoryLdapAuthenticationProvider(String domain) {
// this (domain, null);
// }
/** /**
* @param domain the domain name (may be null or empty) * @param domain the domain name (may be null or empty)
* @param url an LDAP url (or multiple URLs) * @param url an LDAP url (or multiple URLs)
@ -114,7 +109,6 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) { public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) {
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty"); Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null; this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null;
//this.url = StringUtils.hasText(url) ? url : null;
this.url = url; this.url = url;
rootDn = this.domain == null ? null : rootDnFromDomain(this.domain); rootDn = this.domain == null ? null : rootDnFromDomain(this.domain);
} }
@ -122,13 +116,12 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
@Override @Override
protected DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken auth) { protected DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken auth) {
String username = auth.getName(); String username = auth.getName();
String password = (String)auth.getCredentials(); String password = (String) auth.getCredentials();
DirContext ctx = bindAsUser(username, password); DirContext ctx = bindAsUser(username, password);
try { try {
return searchForUser(ctx, username); return searchForUser(ctx, username);
} catch (NamingException e) { } catch (NamingException e) {
logger.error("Failed to locate directory entry for authenticated user: " + username, e); logger.error("Failed to locate directory entry for authenticated user: " + username, e);
throw badCredentials(e); throw badCredentials(e);
@ -168,7 +161,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
// TODO. add DNS lookup based on domain // TODO. add DNS lookup based on domain
final String bindUrl = url; final String bindUrl = url;
Hashtable<String,String> env = new Hashtable<String,String>(); Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_AUTHENTICATION, "simple");
String bindPrincipal = createBindPrincipal(username); String bindPrincipal = createBindPrincipal(username);
env.put(Context.SECURITY_PRINCIPAL, bindPrincipal); env.put(Context.SECURITY_PRINCIPAL, bindPrincipal);
@ -189,25 +182,26 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
} }
} }
void handleBindException(String bindPrincipal, NamingException exception) { private void handleBindException(String bindPrincipal, NamingException exception) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Authentication for " + bindPrincipal + " failed:" + exception); logger.debug("Authentication for " + bindPrincipal + " failed:" + exception);
} }
int subErrorCode = parseSubErrorCode(exception.getMessage()); int subErrorCode = parseSubErrorCode(exception.getMessage());
if (subErrorCode > 0) { if (subErrorCode <= 0) {
logger.info("Active Directory authentication failed: " + subCodeToLogMessage(subErrorCode));
if (convertSubErrorCodesToExceptions) {
raiseExceptionForErrorCode(subErrorCode, exception);
}
} else {
logger.debug("Failed to locate AD-specific sub-error code in message"); 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);
} }
} }
int parseSubErrorCode(String message) { private int parseSubErrorCode(String message) {
Matcher m = SUB_ERROR_CODE.matcher(message); Matcher m = SUB_ERROR_CODE.matcher(message);
if (m.matches()) { if (m.matches()) {
@ -217,7 +211,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
return -1; return -1;
} }
void raiseExceptionForErrorCode(int code, NamingException exception) { private void raiseExceptionForErrorCode(int code, NamingException exception) {
String hexString = Integer.toHexString(code); String hexString = Integer.toHexString(code);
Throwable cause = new ActiveDirectoryAuthenticationException(hexString, exception.getMessage(), exception); Throwable cause = new ActiveDirectoryAuthenticationException(hexString, exception.getMessage(), exception);
switch (code) { switch (code) {
@ -238,7 +232,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
} }
} }
String subCodeToLogMessage(int code) { private String subCodeToLogMessage(int code) {
switch (code) { switch (code) {
case USERNAME_NOT_FOUND: case USERNAME_NOT_FOUND:
return "User was not found in directory"; return "User was not found in directory";
@ -270,28 +264,25 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
return (BadCredentialsException) badCredentials().initCause(cause); return (BadCredentialsException) badCredentials().initCause(cause);
} }
@SuppressWarnings("deprecation") private DirContextOperations searchForUser(DirContext context, String username) throws NamingException {
private DirContextOperations searchForUser(DirContext ctx, String username) throws NamingException { SearchControls searchControls = new SearchControls();
SearchControls searchCtls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))";
final String bindPrincipal = createBindPrincipal(username);
String bindPrincipal = createBindPrincipal(username);
String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal); String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal);
try { try {
return SpringSecurityLdapTemplate.searchForSingleEntryInternal(ctx, searchCtls, searchRoot, searchFilter, return SpringSecurityLdapTemplate.searchForSingleEntryInternal(context, searchControls,
new Object[]{bindPrincipal}); searchRoot, searchFilter, new Object[]{username});
} catch (IncorrectResultSizeDataAccessException incorrectResults) { } catch (IncorrectResultSizeDataAccessException incorrectResults) {
if (incorrectResults.getActualSize() == 0) { // Search should never return multiple results if properly configured - just rethrow
UsernameNotFoundException userNameNotFoundException = new UsernameNotFoundException("User " + username + " not found in directory."); if (incorrectResults.getActualSize() != 0) {
userNameNotFoundException.initCause(incorrectResults); throw incorrectResults;
throw badCredentials(userNameNotFoundException);
} }
// Search should never return multiple results if properly configured, so just rethrow // If we found no results, then the username/password did not match
throw incorrectResults; UsernameNotFoundException userNameNotFoundException = new UsernameNotFoundException("User " + username
+ " not found in directory.", incorrectResults);
throw badCredentials(userNameNotFoundException);
} }
} }
@ -303,7 +294,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
throw badCredentials(); throw badCredentials();
} }
return rootDnFromDomain(bindPrincipal.substring(atChar+ 1, bindPrincipal.length())); return rootDnFromDomain(bindPrincipal.substring(atChar + 1, bindPrincipal.length()));
} }
private String rootDnFromDomain(String domain) { private String rootDnFromDomain(String domain) {
@ -342,6 +333,21 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
this.convertSubErrorCodesToExceptions = 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}.
* <p>
* Defaults to: {@code (&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={0})))}
* </p>
*
* @param searchFilter the filter string
*
* @since 3.2
*/
public void setSearchFilter(String searchFilter) {
this.searchFilter = searchFilter;
}
static class ContextFactory { static class ContextFactory {
DirContext createContext(Hashtable<?,?> env) throws NamingException { DirContext createContext(Hashtable<?,?> env) throws NamingException {
return new InitialLdapContext(env, null); return new InitialLdapContext(env, null);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2014 the original author or authors. * 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 * 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 * the License. You may obtain a copy of the License at
@ -44,6 +44,7 @@ import javax.naming.directory.SearchResult;
import java.util.Hashtable; import java.util.Hashtable;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq; import static org.mockito.Mockito.eq;
@ -97,6 +98,32 @@ public class ActiveDirectoryLdapAuthenticationProviderTests {
assertEquals(1, result.getAuthorities().size()); assertEquals(1, result.getAuthorities().size());
} }
// SEC-1915
@Test
public void customSearchFilterIsUsedForSuccessfulAuthentication() throws Exception {
//given
String customSearchFilter = "(&(objectClass=user)(sAMAccountName={0}))";
DirContext ctx = mock(DirContext.class);
when(ctx.getNameInNamespace()).thenReturn("");
DirContextAdapter dca = new DirContextAdapter();
SearchResult sr = new SearchResult("CN=Joe Jannsen,CN=Users", dca, dca.getAttributes());
when(ctx.search(any(Name.class), eq(customSearchFilter), any(Object[].class), any(SearchControls.class)))
.thenReturn(new MockNamingEnumeration(sr));
ActiveDirectoryLdapAuthenticationProvider customProvider
= new ActiveDirectoryLdapAuthenticationProvider("mydomain.eu", "ldap://192.168.1.200/");
customProvider.contextFactory = createContextFactoryReturning(ctx);
//when
customProvider.setSearchFilter(customSearchFilter);
Authentication result = customProvider.authenticate(joe);
//then
assertTrue(result.isAuthenticated());
}
@Test @Test
public void nullDomainIsSupportedIfAuthenticatingWithFullUserPrincipal() throws Exception { public void nullDomainIsSupportedIfAuthenticatingWithFullUserPrincipal() throws Exception {
provider = new ActiveDirectoryLdapAuthenticationProvider(null, "ldap://192.168.1.200/"); provider = new ActiveDirectoryLdapAuthenticationProvider(null, "ldap://192.168.1.200/");
@ -319,17 +346,4 @@ public class ActiveDirectoryLdapAuthenticationProviderTests {
return next(); return next();
} }
} }
// @Test
// public void realAuthenticationIsSucessful() throws Exception {
// ActiveDirectoryLdapAuthenticationProvider provider =
// new ActiveDirectoryLdapAuthenticationProvider(null, "ldap://192.168.1.200/");
//
// provider.setConvertSubErrorCodesToExceptions(true);
//
// Authentication result = provider.authenticate(new UsernamePasswordAuthenticationToken("luke@fenetres.monkeymachine.eu","p!ssw0rd"));
//
// assertEquals(1, result.getAuthorities().size());
// assertTrue(result.getAuthorities().contains(new SimpleGrantedAuthority("blah")));
// }
} }