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 69d826301ae..ad188215d40 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 @@ -107,6 +107,8 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends private final String url; private boolean convertSubErrorCodesToExceptions; private String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))"; + private String bindUser; + private String bindPassword; // Only used to allow tests to substitute a mock LdapContext ContextFactory contextFactory = new ContextFactory(); @@ -141,10 +143,25 @@ protected DirContextOperations doAuthentication( String username = auth.getName(); String password = (String) auth.getCredentials(); - DirContext ctx = bindAsUser(username, password); + DirContext ctx; + if (StringUtils.hasText(bindUser) && StringUtils.hasText(bindPassword)) { + ctx = bindAsUser(bindUser, bindPassword); + } else { + ctx = bindAsUser(username, password); + } try { - return searchForUser(ctx, username); + DirContextOperations result = searchForUser(ctx, username); + if (StringUtils.hasText(bindUser) && StringUtils.hasText(bindPassword)) { + // attempt another authentication, now with the user's password + Hashtable authEnv = new Hashtable(); + authEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + authEnv.put(Context.PROVIDER_URL, url); + authEnv.put(Context.SECURITY_PRINCIPAL, result.getNameInNamespace()); + authEnv.put(Context.SECURITY_CREDENTIALS, password); + contextFactory.createContext(authEnv); + } + return result; } catch (NamingException e) { logger.error("Failed to locate directory entry for authenticated user: " @@ -397,6 +414,19 @@ public void setSearchFilter(String searchFilter) { this.searchFilter = searchFilter; } + /** + * By default, the connection to the AD server will use the credentials being authenticated. + * If another M2M AD account shall be used for connecting to the AD server, these credentials + * can be provided here. + * + * @param userName the name of the M2M user + * @param password the password of the M2M user + */ + public void setBindUser(String userName, String password) { + this.bindUser = userName; + this.bindPassword = password; + } + static class ContextFactory { DirContext createContext(Hashtable env) throws NamingException { return new InitialLdapContext(env, null); diff --git a/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java b/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java index d6463ca99e5..b1f1cb73a3c 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java +++ b/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java @@ -25,6 +25,8 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.DistinguishedName; @@ -36,12 +38,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import javax.naming.AuthenticationException; -import javax.naming.CommunicationException; -import javax.naming.Name; -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; +import javax.naming.*; import javax.naming.directory.DirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; @@ -393,6 +390,44 @@ public void rootDnProvidedSeparatelyFromDomainAlsoWorks() throws Exception { } + @Test + public void differentUserForBindOperation() throws Exception { + // given + ArgumentCaptor bindCapture = ArgumentCaptor.forClass(Hashtable.class); + ArgumentCaptor userCapture = ArgumentCaptor.forClass(Object[].class); + + final 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), any(String.class), userCapture.capture(), + any(SearchControls.class))).thenReturn( + new MockNamingEnumeration(sr)); + ContextFactory contextFactory = mock(ContextFactory.class); + when(contextFactory.createContext(bindCapture.capture())).thenAnswer(new Answer() { + @Override + public DirContext answer(final InvocationOnMock invocation) throws Throwable { + Hashtable args = (Hashtable) invocation.getArguments()[0]; + assertThat(args.get(Context.SECURITY_PRINCIPAL)).isIn("bindUser@mydomain.eu", ""); + return ctx; + } + }); + + ActiveDirectoryLdapAuthenticationProvider customProvider = new ActiveDirectoryLdapAuthenticationProvider( + "mydomain.eu", "ldap://192.168.1.200/"); + customProvider.contextFactory = contextFactory; + customProvider.setBindUser("bindUser", "bindPassword"); + + // when + Authentication result = customProvider.authenticate(joe); + + // then + assertThat(result.isAuthenticated()).isTrue(); + } + ContextFactory createContextFactoryThrowing(final NamingException e) { return new ContextFactory() { @Override