/* * Copyright 2002-2016 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 * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.security.ldap.authentication.ad; import org.apache.directory.shared.ldap.util.EmptyEnumeration; import org.hamcrest.BaseMatcher; import org.hamcrest.CoreMatchers; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.DistinguishedName; import org.springframework.security.authentication.AccountExpiredException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.LockedException; 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.directory.DirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import java.util.Hashtable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.mockito.Mockito.*; import static org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.ContextFactory; /** * @author Luke Taylor * @author Rob Winch */ public class ActiveDirectoryLdapAuthenticationProviderTests { @Rule public ExpectedException thrown = ExpectedException.none(); ActiveDirectoryLdapAuthenticationProvider provider; UsernamePasswordAuthenticationToken joe = new UsernamePasswordAuthenticationToken( "joe", "password"); @Before public void setUp() throws Exception { provider = new ActiveDirectoryLdapAuthenticationProvider("mydomain.eu", "ldap://192.168.1.200/"); } @Test public void bindPrincipalIsCreatedCorrectly() throws Exception { assertThat(provider.createBindPrincipal("joe")).isEqualTo("joe@mydomain.eu"); assertThat(provider.createBindPrincipal("joe@mydomain.eu")).isEqualTo("joe@mydomain.eu"); } @Test public void successfulAuthenticationProducesExpectedAuthorities() throws Exception { checkAuthentication("dc=mydomain,dc=eu", provider); } // 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 assertThat(result.isAuthenticated()).isTrue(); } @Test public void defaultSearchFilter() throws Exception { // given final String defaultSearchFilter = "(&(objectClass=user)(userPrincipalName={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(defaultSearchFilter), 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 Authentication result = customProvider.authenticate(joe); // then assertThat(result.isAuthenticated()).isTrue(); verify(ctx).search(any(DistinguishedName.class), eq(defaultSearchFilter), any(Object[].class), any(SearchControls.class)); } // SEC-2897 @Test public void bindPrincipalUsed() throws Exception { // given final String defaultSearchFilter = "(&(objectClass=user)(userPrincipalName={0}))"; ArgumentCaptor<Object[]> captor = ArgumentCaptor.forClass(Object[].class); 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(defaultSearchFilter), captor.capture(), any(SearchControls.class))).thenReturn( new MockNamingEnumeration(sr)); ActiveDirectoryLdapAuthenticationProvider customProvider = new ActiveDirectoryLdapAuthenticationProvider( "mydomain.eu", "ldap://192.168.1.200/"); customProvider.contextFactory = createContextFactoryReturning(ctx); // when Authentication result = customProvider.authenticate(joe); // then assertThat(captor.getValue()).containsOnly("joe@mydomain.eu"); assertThat(result.isAuthenticated()).isTrue(); } @Test(expected = IllegalArgumentException.class) public void setSearchFilterNull() { provider.setSearchFilter(null); } @Test(expected = IllegalArgumentException.class) public void setSearchFilterEmpty() { provider.setSearchFilter(" "); } @Test public void nullDomainIsSupportedIfAuthenticatingWithFullUserPrincipal() throws Exception { provider = new ActiveDirectoryLdapAuthenticationProvider(null, "ldap://192.168.1.200/"); 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(eq(new DistinguishedName("DC=mydomain,DC=eu")), any(String.class), any(Object[].class), any(SearchControls.class))) .thenReturn(new MockNamingEnumeration(sr)); provider.contextFactory = createContextFactoryReturning(ctx); try { provider.authenticate(joe); fail("Expected BadCredentialsException for user with no domain information"); } catch (BadCredentialsException expected) { } provider.authenticate(new UsernamePasswordAuthenticationToken("joe@mydomain.eu", "password")); } @Test(expected = BadCredentialsException.class) public void failedUserSearchCausesBadCredentials() throws Exception { DirContext ctx = mock(DirContext.class); when(ctx.getNameInNamespace()).thenReturn(""); when( ctx.search(any(Name.class), any(String.class), any(Object[].class), any(SearchControls.class))) .thenThrow(new NameNotFoundException()); provider.contextFactory = createContextFactoryReturning(ctx); provider.authenticate(joe); } // SEC-2017 @Test(expected = BadCredentialsException.class) public void noUserSearchCausesUsernameNotFound() throws Exception { DirContext ctx = mock(DirContext.class); when(ctx.getNameInNamespace()).thenReturn(""); when( ctx.search(any(Name.class), any(String.class), any(Object[].class), any(SearchControls.class))).thenReturn( new EmptyEnumeration<SearchResult>()); provider.contextFactory = createContextFactoryReturning(ctx); provider.authenticate(joe); } // SEC-2500 @Test(expected = BadCredentialsException.class) public void sec2500PreventAnonymousBind() { provider.authenticate(new UsernamePasswordAuthenticationToken("rwinch", "")); } @SuppressWarnings("unchecked") @Test(expected = IncorrectResultSizeDataAccessException.class) public void duplicateUserSearchCausesError() throws Exception { DirContext ctx = mock(DirContext.class); when(ctx.getNameInNamespace()).thenReturn(""); NamingEnumeration<SearchResult> searchResults = mock(NamingEnumeration.class); when(searchResults.hasMore()).thenReturn(true, true, false); SearchResult searchResult = mock(SearchResult.class); when(searchResult.getObject()).thenReturn(new DirContextAdapter("ou=1"), new DirContextAdapter("ou=2")); when(searchResults.next()).thenReturn(searchResult); when( ctx.search(any(Name.class), any(String.class), any(Object[].class), any(SearchControls.class))).thenReturn(searchResults); provider.contextFactory = createContextFactoryReturning(ctx); provider.authenticate(joe); } static final String msg = "[LDAP: error code 49 - 80858585: LdapErr: DSID-DECAFF0, comment: AcceptSecurityContext error, data "; @Test(expected = BadCredentialsException.class) public void userNotFoundIsCorrectlyMapped() { provider.contextFactory = createContextFactoryThrowing(new AuthenticationException( msg + "525, xxxx]")); provider.setConvertSubErrorCodesToExceptions(true); provider.authenticate(joe); } @Test(expected = BadCredentialsException.class) public void incorrectPasswordIsCorrectlyMapped() { provider.contextFactory = createContextFactoryThrowing(new AuthenticationException( msg + "52e, xxxx]")); provider.setConvertSubErrorCodesToExceptions(true); provider.authenticate(joe); } @Test(expected = BadCredentialsException.class) public void notPermittedIsCorrectlyMapped() { provider.contextFactory = createContextFactoryThrowing(new AuthenticationException( msg + "530, xxxx]")); provider.setConvertSubErrorCodesToExceptions(true); provider.authenticate(joe); } @Test public void passwordNeedsResetIsCorrectlyMapped() { final String dataCode = "773"; provider.contextFactory = createContextFactoryThrowing(new AuthenticationException( msg + dataCode + ", xxxx]")); provider.setConvertSubErrorCodesToExceptions(true); thrown.expect(BadCredentialsException.class); thrown.expect(new BaseMatcher<BadCredentialsException>() { private Matcher<Object> causeInstance = CoreMatchers .instanceOf(ActiveDirectoryAuthenticationException.class); private Matcher<String> causeDataCode = CoreMatchers.equalTo(dataCode); public boolean matches(Object that) { Throwable t = (Throwable) that; ActiveDirectoryAuthenticationException cause = (ActiveDirectoryAuthenticationException) t .getCause(); return causeInstance.matches(cause) && causeDataCode.matches(cause.getDataCode()); } public void describeTo(Description desc) { desc.appendText("getCause() "); causeInstance.describeTo(desc); desc.appendText("getCause().getDataCode() "); causeDataCode.describeTo(desc); } }); provider.authenticate(joe); } @Test(expected = CredentialsExpiredException.class) public void expiredPasswordIsCorrectlyMapped() { provider.contextFactory = createContextFactoryThrowing(new AuthenticationException( msg + "532, xxxx]")); try { provider.authenticate(joe); fail("BadCredentialsException should had been thrown"); } catch (BadCredentialsException expected) { } provider.setConvertSubErrorCodesToExceptions(true); provider.authenticate(joe); } @Test(expected = DisabledException.class) public void accountDisabledIsCorrectlyMapped() { provider.contextFactory = createContextFactoryThrowing(new AuthenticationException( msg + "533, xxxx]")); provider.setConvertSubErrorCodesToExceptions(true); provider.authenticate(joe); } @Test(expected = AccountExpiredException.class) public void accountExpiredIsCorrectlyMapped() { provider.contextFactory = createContextFactoryThrowing(new AuthenticationException( msg + "701, xxxx]")); provider.setConvertSubErrorCodesToExceptions(true); provider.authenticate(joe); } @Test(expected = LockedException.class) public void accountLockedIsCorrectlyMapped() { provider.contextFactory = createContextFactoryThrowing(new AuthenticationException( msg + "775, xxxx]")); provider.setConvertSubErrorCodesToExceptions(true); provider.authenticate(joe); } @Test(expected = BadCredentialsException.class) public void unknownErrorCodeIsCorrectlyMapped() { provider.contextFactory = createContextFactoryThrowing(new AuthenticationException( msg + "999, xxxx]")); provider.setConvertSubErrorCodesToExceptions(true); provider.authenticate(joe); } @Test(expected = BadCredentialsException.class) public void errorWithNoSubcodeIsHandledCleanly() throws Exception { provider.contextFactory = createContextFactoryThrowing(new AuthenticationException( msg)); provider.setConvertSubErrorCodesToExceptions(true); provider.authenticate(joe); } @Test(expected = org.springframework.ldap.CommunicationException.class) public void nonAuthenticationExceptionIsConvertedToSpringLdapException() throws Exception { provider.contextFactory = createContextFactoryThrowing(new CommunicationException( msg)); provider.authenticate(joe); } @Test public void rootDnProvidedSeparatelyFromDomainAlsoWorks() throws Exception { ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider( "mydomain.eu", "ldap://192.168.1.200/", "dc=ad,dc=eu,dc=mydomain"); checkAuthentication("dc=ad,dc=eu,dc=mydomain", provider); } ContextFactory createContextFactoryThrowing(final NamingException e) { return new ContextFactory() { @Override DirContext createContext(Hashtable<?, ?> env) throws NamingException { throw e; } }; } ContextFactory createContextFactoryReturning(final DirContext ctx) { return new ContextFactory() { @Override DirContext createContext(Hashtable<?, ?> env) throws NamingException { return ctx; } }; } private void checkAuthentication(String rootDn, ActiveDirectoryLdapAuthenticationProvider provider) throws NamingException { 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()); @SuppressWarnings("deprecation") DistinguishedName searchBaseDn = new DistinguishedName(rootDn); when( ctx.search(eq(searchBaseDn), any(String.class), any(Object[].class), any(SearchControls.class))).thenReturn( new MockNamingEnumeration(sr)).thenReturn(new MockNamingEnumeration(sr)); provider.contextFactory = createContextFactoryReturning(ctx); Authentication result = provider.authenticate(joe); assertThat(result.getAuthorities()).isEmpty(); dca.addAttributeValue("memberOf", "CN=Admin,CN=Users,DC=mydomain,DC=eu"); result = provider.authenticate(joe); assertThat(result.getAuthorities()).hasSize(1); } static class MockNamingEnumeration implements NamingEnumeration<SearchResult> { private SearchResult sr; public MockNamingEnumeration(SearchResult sr) { this.sr = sr; } public SearchResult next() { SearchResult result = sr; sr = null; return result; } public boolean hasMore() { return sr != null; } public void close() { } public boolean hasMoreElements() { return hasMore(); } public SearchResult nextElement() { return next(); } } }