Convert to use LdapClient instead of SpringSecurityLdapTemplate in PasswordComparisonAuthenticator

Signed-off-by: minkukjo <strike0115@naver.com>
This commit is contained in:
minkukjo 2025-06-21 17:47:49 +09:00
parent 08cbdb4640
commit 03fdfc9e3d
3 changed files with 138 additions and 69 deletions

View File

@ -20,9 +20,15 @@ import org.apache.commons.logging.Log;
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.codec.Utf8;
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 final class PasswordComparisonAuthenticator extends AbstractLdapAuthentic
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<DirContextOperations>) 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 final class PasswordComparisonAuthenticator extends AbstractLdapAuthentic
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 @@ public final class PasswordComparisonAuthenticator extends AbstractLdapAuthentic
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<String>) attrs -> this.passwordAttributeName)
.isEmpty();
}
public void setPasswordAttributeName(String passwordAttribute) {

View File

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

View File

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