Skip to content

Commit c54346b

Browse files
MateuszRasinskiRob Winch
authored and
Rob Winch
committed
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'
1 parent 5f57e5b commit c54346b

File tree

2 files changed

+79
-59
lines changed

2 files changed

+79
-59
lines changed

ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java

+51-45
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
55
* the License. You may obtain a copy of the License at
@@ -47,11 +47,12 @@
4747
* Specialized LDAP authentication provider which uses Active Directory configuration conventions.
4848
* <p>
4949
* It will authenticate using the Active Directory
50-
* <a href="http://msdn.microsoft.com/en-us/library/ms680857%28VS.85%29.aspx">{@code userPrincipalName}</a>
51-
* (in the form {@code username@domain}). If the username does not already end with the domain name, the
52-
* {@code userPrincipalName} will be built by appending the configured domain name to the username supplied in the
53-
* authentication request. If no domain name is configured, it is assumed that the username will always contain the
54-
* domain name.
50+
* <a href="http://msdn.microsoft.com/en-us/library/ms680857%28VS.85%29.aspx">{@code userPrincipalName}</a> or
51+
* <a href="http://msdn.microsoft.com/en-us/library/ms679635%28v=vs.85%29.aspx">{@code sAMAccountName}</a> (or a custom
52+
* {@link #setSearchFilter(String) searchFilter}) in the form {@code username@domain}. If the username does not
53+
* already end with the domain name, the {@code userPrincipalName} will be built by appending the configured domain
54+
* name to the username supplied in the authentication request. If no domain name is configured, it is assumed that
55+
* the username will always contain the domain name.
5556
* <p>
5657
* The user authorities are obtained from the data contained in the {@code memberOf} attribute.
5758
*
@@ -96,39 +97,31 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
9697
private final String rootDn;
9798
private final String url;
9899
private boolean convertSubErrorCodesToExceptions;
100+
private String searchFilter = "(&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={0})))";
99101

100102
// Only used to allow tests to substitute a mock LdapContext
101103
ContextFactory contextFactory = new ContextFactory();
102104

103-
/**
104-
* @param domain the domain for which authentication should take place
105-
*/
106-
// public ActiveDirectoryLdapAuthenticationProvider(String domain) {
107-
// this (domain, null);
108-
// }
109-
110105
/**
111106
* @param domain the domain name (may be null or empty)
112107
* @param url an LDAP url (or multiple URLs)
113108
*/
114109
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) {
115110
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
116111
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null;
117-
//this.url = StringUtils.hasText(url) ? url : null;
118112
this.url = url;
119113
rootDn = this.domain == null ? null : rootDnFromDomain(this.domain);
120114
}
121115

122116
@Override
123117
protected DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken auth) {
124118
String username = auth.getName();
125-
String password = (String)auth.getCredentials();
119+
String password = (String) auth.getCredentials();
126120

127121
DirContext ctx = bindAsUser(username, password);
128122

129123
try {
130124
return searchForUser(ctx, username);
131-
132125
} catch (NamingException e) {
133126
logger.error("Failed to locate directory entry for authenticated user: " + username, e);
134127
throw badCredentials(e);
@@ -168,7 +161,7 @@ private DirContext bindAsUser(String username, String password) {
168161
// TODO. add DNS lookup based on domain
169162
final String bindUrl = url;
170163

171-
Hashtable<String,String> env = new Hashtable<String,String>();
164+
Hashtable<String, String> env = new Hashtable<String, String>();
172165
env.put(Context.SECURITY_AUTHENTICATION, "simple");
173166
String bindPrincipal = createBindPrincipal(username);
174167
env.put(Context.SECURITY_PRINCIPAL, bindPrincipal);
@@ -189,25 +182,26 @@ private DirContext bindAsUser(String username, String password) {
189182
}
190183
}
191184

192-
void handleBindException(String bindPrincipal, NamingException exception) {
185+
private void handleBindException(String bindPrincipal, NamingException exception) {
193186
if (logger.isDebugEnabled()) {
194187
logger.debug("Authentication for " + bindPrincipal + " failed:" + exception);
195188
}
196189

197190
int subErrorCode = parseSubErrorCode(exception.getMessage());
198191

199-
if (subErrorCode > 0) {
200-
logger.info("Active Directory authentication failed: " + subCodeToLogMessage(subErrorCode));
201-
202-
if (convertSubErrorCodesToExceptions) {
203-
raiseExceptionForErrorCode(subErrorCode, exception);
204-
}
205-
} else {
192+
if (subErrorCode <= 0) {
206193
logger.debug("Failed to locate AD-specific sub-error code in message");
194+
return;
195+
}
196+
197+
logger.info("Active Directory authentication failed: " + subCodeToLogMessage(subErrorCode));
198+
199+
if (convertSubErrorCodesToExceptions) {
200+
raiseExceptionForErrorCode(subErrorCode, exception);
207201
}
208202
}
209203

210-
int parseSubErrorCode(String message) {
204+
private int parseSubErrorCode(String message) {
211205
Matcher m = SUB_ERROR_CODE.matcher(message);
212206

213207
if (m.matches()) {
@@ -217,7 +211,7 @@ int parseSubErrorCode(String message) {
217211
return -1;
218212
}
219213

220-
void raiseExceptionForErrorCode(int code, NamingException exception) {
214+
private void raiseExceptionForErrorCode(int code, NamingException exception) {
221215
String hexString = Integer.toHexString(code);
222216
Throwable cause = new ActiveDirectoryAuthenticationException(hexString, exception.getMessage(), exception);
223217
switch (code) {
@@ -238,7 +232,7 @@ void raiseExceptionForErrorCode(int code, NamingException exception) {
238232
}
239233
}
240234

241-
String subCodeToLogMessage(int code) {
235+
private String subCodeToLogMessage(int code) {
242236
switch (code) {
243237
case USERNAME_NOT_FOUND:
244238
return "User was not found in directory";
@@ -270,28 +264,25 @@ private BadCredentialsException badCredentials(Throwable cause) {
270264
return (BadCredentialsException) badCredentials().initCause(cause);
271265
}
272266

273-
@SuppressWarnings("deprecation")
274-
private DirContextOperations searchForUser(DirContext ctx, String username) throws NamingException {
275-
SearchControls searchCtls = new SearchControls();
276-
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
277-
278-
String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))";
279-
280-
final String bindPrincipal = createBindPrincipal(username);
267+
private DirContextOperations searchForUser(DirContext context, String username) throws NamingException {
268+
SearchControls searchControls = new SearchControls();
269+
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
281270

271+
String bindPrincipal = createBindPrincipal(username);
282272
String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal);
283273

284274
try {
285-
return SpringSecurityLdapTemplate.searchForSingleEntryInternal(ctx, searchCtls, searchRoot, searchFilter,
286-
new Object[]{bindPrincipal});
275+
return SpringSecurityLdapTemplate.searchForSingleEntryInternal(context, searchControls,
276+
searchRoot, searchFilter, new Object[]{username});
287277
} catch (IncorrectResultSizeDataAccessException incorrectResults) {
288-
if (incorrectResults.getActualSize() == 0) {
289-
UsernameNotFoundException userNameNotFoundException = new UsernameNotFoundException("User " + username + " not found in directory.");
290-
userNameNotFoundException.initCause(incorrectResults);
291-
throw badCredentials(userNameNotFoundException);
278+
// Search should never return multiple results if properly configured - just rethrow
279+
if (incorrectResults.getActualSize() != 0) {
280+
throw incorrectResults;
292281
}
293-
// Search should never return multiple results if properly configured, so just rethrow
294-
throw incorrectResults;
282+
// If we found no results, then the username/password did not match
283+
UsernameNotFoundException userNameNotFoundException = new UsernameNotFoundException("User " + username
284+
+ " not found in directory.", incorrectResults);
285+
throw badCredentials(userNameNotFoundException);
295286
}
296287
}
297288

@@ -303,7 +294,7 @@ private String searchRootFromPrincipal(String bindPrincipal) {
303294
throw badCredentials();
304295
}
305296

306-
return rootDnFromDomain(bindPrincipal.substring(atChar+ 1, bindPrincipal.length()));
297+
return rootDnFromDomain(bindPrincipal.substring(atChar + 1, bindPrincipal.length()));
307298
}
308299

309300
private String rootDnFromDomain(String domain) {
@@ -342,6 +333,21 @@ public void setConvertSubErrorCodesToExceptions(boolean convertSubErrorCodesToEx
342333
this.convertSubErrorCodesToExceptions = convertSubErrorCodesToExceptions;
343334
}
344335

336+
/**
337+
* The LDAP filter string to search for the user being authenticated.
338+
* Occurrences of {0} are replaced with the {@code username@domain}.
339+
* <p>
340+
* Defaults to: {@code (&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={0})))}
341+
* </p>
342+
*
343+
* @param searchFilter the filter string
344+
*
345+
* @since 3.2
346+
*/
347+
public void setSearchFilter(String searchFilter) {
348+
this.searchFilter = searchFilter;
349+
}
350+
345351
static class ContextFactory {
346352
DirContext createContext(Hashtable<?,?> env) throws NamingException {
347353
return new InitialLdapContext(env, null);

ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java

+28-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
55
* the License. You may obtain a copy of the License at
@@ -44,6 +44,7 @@
4444
import java.util.Hashtable;
4545

4646
import static org.junit.Assert.assertEquals;
47+
import static org.junit.Assert.assertTrue;
4748
import static org.junit.Assert.fail;
4849
import static org.mockito.Mockito.any;
4950
import static org.mockito.Mockito.eq;
@@ -97,6 +98,32 @@ public void successfulAuthenticationProducesExpectedAuthorities() throws Excepti
9798
assertEquals(1, result.getAuthorities().size());
9899
}
99100

101+
// SEC-1915
102+
@Test
103+
public void customSearchFilterIsUsedForSuccessfulAuthentication() throws Exception {
104+
//given
105+
String customSearchFilter = "(&(objectClass=user)(sAMAccountName={0}))";
106+
107+
DirContext ctx = mock(DirContext.class);
108+
when(ctx.getNameInNamespace()).thenReturn("");
109+
110+
DirContextAdapter dca = new DirContextAdapter();
111+
SearchResult sr = new SearchResult("CN=Joe Jannsen,CN=Users", dca, dca.getAttributes());
112+
when(ctx.search(any(Name.class), eq(customSearchFilter), any(Object[].class), any(SearchControls.class)))
113+
.thenReturn(new MockNamingEnumeration(sr));
114+
115+
ActiveDirectoryLdapAuthenticationProvider customProvider
116+
= new ActiveDirectoryLdapAuthenticationProvider("mydomain.eu", "ldap://192.168.1.200/");
117+
customProvider.contextFactory = createContextFactoryReturning(ctx);
118+
119+
//when
120+
customProvider.setSearchFilter(customSearchFilter);
121+
Authentication result = customProvider.authenticate(joe);
122+
123+
//then
124+
assertTrue(result.isAuthenticated());
125+
}
126+
100127
@Test
101128
public void nullDomainIsSupportedIfAuthenticatingWithFullUserPrincipal() throws Exception {
102129
provider = new ActiveDirectoryLdapAuthenticationProvider(null, "ldap://192.168.1.200/");
@@ -319,17 +346,4 @@ public SearchResult nextElement() {
319346
return next();
320347
}
321348
}
322-
323-
// @Test
324-
// public void realAuthenticationIsSucessful() throws Exception {
325-
// ActiveDirectoryLdapAuthenticationProvider provider =
326-
// new ActiveDirectoryLdapAuthenticationProvider(null, "ldap://192.168.1.200/");
327-
//
328-
// provider.setConvertSubErrorCodesToExceptions(true);
329-
//
330-
// Authentication result = provider.authenticate(new UsernamePasswordAuthenticationToken("[email protected]","p!ssw0rd"));
331-
//
332-
// assertEquals(1, result.getAuthorities().size());
333-
// assertTrue(result.getAuthorities().contains(new SimpleGrantedAuthority("blah")));
334-
// }
335349
}

0 commit comments

Comments
 (0)