Skip to content

SEC-1915: Custom ActiveDirectory search filter #157

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
* the License. You may obtain a copy of the License at
Expand Down Expand Up @@ -47,11 +47,12 @@
* Specialized LDAP authentication provider which uses Active Directory configuration conventions.
* <p>
* It will authenticate using the Active Directory
* <a href="http://msdn.microsoft.com/en-us/library/ms680857%28VS.85%29.aspx">{@code userPrincipalName}</a>
* (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.
* <a href="http://msdn.microsoft.com/en-us/library/ms680857%28VS.85%29.aspx">{@code userPrincipalName}</a> or
* <a href="http://msdn.microsoft.com/en-us/library/ms679635%28v=vs.85%29.aspx">{@code sAMAccountName}</a> (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.
* <p>
* The user authorities are obtained from the data contained in the {@code memberOf} attribute.
*
Expand Down Expand Up @@ -96,39 +97,31 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
private final String rootDn;
private final String url;
private boolean convertSubErrorCodesToExceptions;
private String searchFilter = "(&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={0})))";

// Only used to allow tests to substitute a mock LdapContext
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 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 = StringUtils.hasText(url) ? url : 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();
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);
Expand Down Expand Up @@ -168,7 +161,7 @@ private DirContext bindAsUser(String username, String password) {
// TODO. add DNS lookup based on domain
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");
String bindPrincipal = createBindPrincipal(username);
env.put(Context.SECURITY_PRINCIPAL, bindPrincipal);
Expand All @@ -189,25 +182,26 @@ private DirContext bindAsUser(String username, String password) {
}
}

void handleBindException(String bindPrincipal, NamingException exception) {
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.info("Active Directory authentication failed: " + subCodeToLogMessage(subErrorCode));

if (convertSubErrorCodesToExceptions) {
raiseExceptionForErrorCode(subErrorCode, exception);
}
} else {
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);
}
}

int parseSubErrorCode(String message) {
private int parseSubErrorCode(String message) {
Matcher m = SUB_ERROR_CODE.matcher(message);

if (m.matches()) {
Expand All @@ -217,7 +211,7 @@ int parseSubErrorCode(String message) {
return -1;
}

void raiseExceptionForErrorCode(int code, NamingException exception) {
private void raiseExceptionForErrorCode(int code, NamingException exception) {
String hexString = Integer.toHexString(code);
Throwable cause = new ActiveDirectoryAuthenticationException(hexString, exception.getMessage(), exception);
switch (code) {
Expand All @@ -238,7 +232,7 @@ void raiseExceptionForErrorCode(int code, NamingException exception) {
}
}

String subCodeToLogMessage(int code) {
private String subCodeToLogMessage(int code) {
switch (code) {
case USERNAME_NOT_FOUND:
return "User was not found in directory";
Expand Down Expand Up @@ -270,28 +264,24 @@ private BadCredentialsException badCredentials(Throwable cause) {
return (BadCredentialsException) badCredentials().initCause(cause);
}

@SuppressWarnings("deprecation")
private DirContextOperations searchForUser(DirContext ctx, String username) throws NamingException {
SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);

String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))";

final String bindPrincipal = createBindPrincipal(username);
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(ctx, searchCtls, searchRoot, searchFilter,
new Object[]{bindPrincipal});
return SpringSecurityLdapTemplate.searchForSingleEntryInternal(context, searchControls,
searchRoot, searchFilter, new Object[]{username});
} catch (IncorrectResultSizeDataAccessException incorrectResults) {
if (incorrectResults.getActualSize() == 0) {
UsernameNotFoundException userNameNotFoundException = new UsernameNotFoundException("User " + username + " not found in directory.", username);
userNameNotFoundException.initCause(incorrectResults);
throw badCredentials(userNameNotFoundException);
// Search should never return multiple results if properly configured - just rethrow
if (incorrectResults.getActualSize() != 0) {
throw incorrectResults;
}
// Search should never return multiple results if properly configured, so just rethrow
throw incorrectResults;
UsernameNotFoundException userNameNotFoundException = new UsernameNotFoundException("User " + username
+ " not found in directory.", incorrectResults);
throw badCredentials(userNameNotFoundException);
}
}

Expand All @@ -303,7 +293,7 @@ private String searchRootFromPrincipal(String bindPrincipal) {
throw badCredentials();
}

return rootDnFromDomain(bindPrincipal.substring(atChar+ 1, bindPrincipal.length()));
return rootDnFromDomain(bindPrincipal.substring(atChar + 1, bindPrincipal.length()));
}

private String rootDnFromDomain(String domain) {
Expand Down Expand Up @@ -342,6 +332,21 @@ public void setConvertSubErrorCodesToExceptions(boolean convertSubErrorCodesToEx
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 {
DirContext createContext(Hashtable<?,?> env) throws NamingException {
return new InitialLdapContext(env, null);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
* the License. You may obtain a copy of the License at
Expand Down Expand Up @@ -44,6 +44,7 @@
import java.util.Hashtable;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
Expand Down Expand Up @@ -97,6 +98,32 @@ public void successfulAuthenticationProducesExpectedAuthorities() throws Excepti
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
public void nullDomainIsSupportedIfAuthenticatingWithFullUserPrincipal() throws Exception {
provider = new ActiveDirectoryLdapAuthenticationProvider(null, "ldap://192.168.1.200/");
Expand Down Expand Up @@ -319,17 +346,4 @@ public SearchResult nextElement() {
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("[email protected]","p!ssw0rd"));
//
// assertEquals(1, result.getAuthorities().size());
// assertTrue(result.getAuthorities().contains(new SimpleGrantedAuthority("blah")));
// }
}