diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticator.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticator.java index e158e87235c..66b5ba5d34e 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticator.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticator.java @@ -20,9 +20,15 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.log.LogMessage; -import org.springframework.ldap.NameNotFoundException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.LdapClient; import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.ldap.query.LdapQueryBuilder; +import org.springframework.ldap.query.SearchScope; +import org.springframework.ldap.support.LdapUtils; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -31,7 +37,6 @@ import org.springframework.security.crypto.keygen.KeyGenerators; import org.springframework.security.crypto.password.LdapShaPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.ldap.SpringSecurityLdapTemplate; import org.springframework.util.Assert; /** @@ -58,8 +63,11 @@ public final class PasswordComparisonAuthenticator extends AbstractLdapAuthentic private boolean usePasswordAttrCompare = false; + LdapClient ldapClient; + public PasswordComparisonAuthenticator(BaseLdapPathContextSource contextSource) { super(contextSource); + ldapClient = LdapClient.builder().contextSource(contextSource).build(); } @Override @@ -70,12 +78,19 @@ public DirContextOperations authenticate(final Authentication authentication) { DirContextOperations user = null; String username = authentication.getName(); String password = (String) authentication.getCredentials(); - SpringSecurityLdapTemplate ldapTemplate = new SpringSecurityLdapTemplate(getContextSource()); for (String userDn : getUserDns(username)) { try { - user = ldapTemplate.retrieveEntry(userDn, getUserAttributes()); + user = this.ldapClient.search() + .query(LdapQueryBuilder.query() + .base(userDn) + .searchScope(SearchScope.OBJECT) + .attributes(getUserAttributes())) + .toObject((AttributesMapper) attrs -> { + BaseLdapPathContextSource source = (BaseLdapPathContextSource) getContextSource(); + return new DirContextAdapter(attrs, LdapUtils.newLdapName(userDn), source.getBaseLdapName()); + }); } - catch (NameNotFoundException ignore) { + catch (EmptyResultDataAccessException ignore) { logger.trace(LogMessage.format("Failed to retrieve user with %s", userDn), ignore); } if (user != null) { @@ -104,7 +119,7 @@ public DirContextOperations authenticate(final Authentication authentication) { this.passwordAttributeName, user.getDn())); return user; } - if (isLdapPasswordCompare(user, ldapTemplate, password)) { + if (isLdapPasswordCompare(user, password)) { logger.debug(LogMessage.format("LDAP-matched password attribute '%s' for user '%s'", this.passwordAttributeName, user.getDn())); return user; @@ -129,11 +144,18 @@ private String getPassword(DirContextOperations user) { return String.valueOf(passwordAttrValue); } - private boolean isLdapPasswordCompare(DirContextOperations user, SpringSecurityLdapTemplate ldapTemplate, - String password) { + private boolean isLdapPasswordCompare(DirContextOperations user, String password) { String encodedPassword = this.passwordEncoder.encode(password); byte[] passwordBytes = Utf8.encode(encodedPassword); - return ldapTemplate.compare(user.getDn().toString(), this.passwordAttributeName, passwordBytes); + return !this.ldapClient.search() + .query(LdapQueryBuilder.query() + .base(user.getDn().toString()) + .searchScope(SearchScope.OBJECT) + .countLimit(1) + .attributes(this.passwordAttributeName) + .filter("({0}={1})", this.passwordAttributeName, passwordBytes)) + .toList((AttributesMapper) attrs -> this.passwordAttributeName) + .isEmpty(); } public void setPasswordAttributeName(String passwordAttribute) { diff --git a/ldap/src/test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorMockTests.java b/ldap/src/test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorMockTests.java deleted file mode 100644 index f5c2d8b4aed..00000000000 --- a/ldap/src/test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorMockTests.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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 - * - * https://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; - -import javax.naming.NamingEnumeration; -import javax.naming.directory.BasicAttribute; -import javax.naming.directory.BasicAttributes; -import javax.naming.directory.DirContext; -import javax.naming.directory.SearchControls; - -import org.junit.jupiter.api.Test; - -import org.springframework.ldap.core.support.BaseLdapPathContextSource; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * @author Luke Taylor - */ -public class PasswordComparisonAuthenticatorMockTests { - - @Test - public void ldapCompareOperationIsUsedWhenPasswordIsNotRetrieved() throws Exception { - final DirContext dirCtx = mock(DirContext.class); - final BaseLdapPathContextSource source = mock(BaseLdapPathContextSource.class); - final BasicAttributes attrs = new BasicAttributes(); - attrs.put(new BasicAttribute("uid", "bob")); - PasswordComparisonAuthenticator authenticator = new PasswordComparisonAuthenticator(source); - authenticator.setUserDnPatterns(new String[] { "cn={0},ou=people" }); - // Get the mock to return an empty attribute set - given(source.getReadOnlyContext()).willReturn(dirCtx); - given(dirCtx.getAttributes(eq("cn=Bob,ou=people"), any(String[].class))).willReturn(attrs); - given(dirCtx.getNameInNamespace()).willReturn("dc=springframework,dc=org"); - // Setup a single return value (i.e. success) - final NamingEnumeration searchResults = new BasicAttributes("", null).getAll(); - given(dirCtx.search(eq("cn=Bob,ou=people"), eq("(userPassword={0})"), any(Object[].class), - any(SearchControls.class))) - .willReturn(searchResults); - authenticator.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("Bob", "bobspassword")); - } - -} diff --git a/ldap/src/test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorUnitTests.java b/ldap/src/test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorUnitTests.java new file mode 100644 index 00000000000..469a467562b --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorUnitTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2025 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 not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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; + +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.LdapClient; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.ldap.query.LdapQuery; +import org.springframework.ldap.support.LdapUtils; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.ldap.search.LdapUserSearch; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Unit tests for {@link PasswordComparisonAuthenticator}. + * + * @author Minkuk Jo + */ +@ExtendWith(MockitoExtension.class) +class PasswordComparisonAuthenticatorUnitTests { + + @Mock + BaseLdapPathContextSource contextSource; + + @InjectMocks + PasswordComparisonAuthenticator authenticator; + + @Mock + LdapClient ldapClient; + + @Mock + LdapClient.SearchSpec searchSpec; + + @Test + void authenticateWhenUserNotFoundThenThrowsUsernameNotFoundException() { + this.authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" }); + this.authenticator.ldapClient = this.ldapClient; + UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken.unauthenticated("user", + "password"); + given(this.ldapClient.search()).willReturn(this.searchSpec); + given(this.searchSpec.query(any(LdapQuery.class))).willReturn(this.searchSpec); + given(this.searchSpec.toObject(any(AttributesMapper.class))).willThrow(new EmptyResultDataAccessException(1)); + LdapUserSearch userSearch = mock(LdapUserSearch.class); + this.authenticator.setUserSearch(userSearch); + given(userSearch.searchForUser("user")).willReturn(null); + + assertThatExceptionOfType(UsernameNotFoundException.class) + .isThrownBy(() -> this.authenticator.authenticate(authentication)) + .withMessage("user not found"); + verifyNoInteractions(this.contextSource); + } + + + @Test + void authenticateWhenPasswordCompareFailsThenThrowsBadCredentialsException() { + this.authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" }); + this.authenticator.ldapClient = this.ldapClient; + UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken.unauthenticated("user", + "password"); + + DirContextOperations user = mock(DirContextOperations.class); + LdapClient.SearchSpec userSearchSpec = mock(LdapClient.SearchSpec.class); + given(user.getDn()).willReturn(LdapUtils.newLdapName("uid=user,ou=people")); + given(userSearchSpec.query(any(LdapQuery.class))).willReturn(userSearchSpec); + given(userSearchSpec.toObject(any(AttributesMapper.class))).willReturn(user); + + LdapClient.SearchSpec passwordSearchSpec = mock(LdapClient.SearchSpec.class); + given(passwordSearchSpec.query(any(LdapQuery.class))).willReturn(passwordSearchSpec); + given(passwordSearchSpec.toList(any(AttributesMapper.class))).willReturn(Collections.emptyList()); + + given(this.ldapClient.search()).willReturn(userSearchSpec, passwordSearchSpec); + + assertThatExceptionOfType(BadCredentialsException.class) + .isThrownBy(() -> this.authenticator.authenticate(authentication)); + } +}