Forráskód Böngészése

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'
Mateusz Rasiński 10 éve
szülő
commit
72bc6bf539

+ 52 - 45
ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java

@@ -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>
- * (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>
  * <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)(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) {
-            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");
             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 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);
         String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal);
 
 
         try {
         try {
-            return SpringSecurityLdapTemplate.searchForSingleEntryInternal(ctx, searchCtls, searchRoot, searchFilter,
-                new Object[]{bindPrincipal});
+            return SpringSecurityLdapTemplate.searchForSingleEntryInternal(context, searchControls,
+                    searchRoot, searchFilter, new Object[]{username});
         } catch (IncorrectResultSizeDataAccessException incorrectResults) {
         } 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;
+            // 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);
         }
         }
     }
     }
 
 
@@ -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,22 @@ 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)(userPrincipalName={0}))}
+     * </p>
+     *
+     * @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 {
     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);

+ 28 - 14
ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java

@@ -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")));
-//    }
 }
 }