Skip to content

Commit 143eca9

Browse files
committed
Convert to use LdapClient instead of SpringSecurityLdapTemplate in PasswordComparisonAuthenticator
1 parent 08cbdb4 commit 143eca9

File tree

3 files changed

+138
-69
lines changed

3 files changed

+138
-69
lines changed

ldap/src/main/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticator.java

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@
2020
import org.apache.commons.logging.LogFactory;
2121

2222
import org.springframework.core.log.LogMessage;
23-
import org.springframework.ldap.NameNotFoundException;
23+
import org.springframework.dao.EmptyResultDataAccessException;
24+
import org.springframework.ldap.core.AttributesMapper;
25+
import org.springframework.ldap.core.DirContextAdapter;
2426
import org.springframework.ldap.core.DirContextOperations;
27+
import org.springframework.ldap.core.LdapClient;
2528
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
29+
import org.springframework.ldap.query.LdapQueryBuilder;
30+
import org.springframework.ldap.query.SearchScope;
31+
import org.springframework.ldap.support.LdapUtils;
2632
import org.springframework.security.authentication.BadCredentialsException;
2733
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
2834
import org.springframework.security.core.Authentication;
@@ -31,7 +37,6 @@
3137
import org.springframework.security.crypto.keygen.KeyGenerators;
3238
import org.springframework.security.crypto.password.LdapShaPasswordEncoder;
3339
import org.springframework.security.crypto.password.PasswordEncoder;
34-
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
3540
import org.springframework.util.Assert;
3641

3742
/**
@@ -58,8 +63,11 @@ public final class PasswordComparisonAuthenticator extends AbstractLdapAuthentic
5863

5964
private boolean usePasswordAttrCompare = false;
6065

66+
LdapClient ldapClient;
67+
6168
public PasswordComparisonAuthenticator(BaseLdapPathContextSource contextSource) {
6269
super(contextSource);
70+
ldapClient = LdapClient.builder().contextSource(contextSource).build();
6371
}
6472

6573
@Override
@@ -70,12 +78,19 @@ public DirContextOperations authenticate(final Authentication authentication) {
7078
DirContextOperations user = null;
7179
String username = authentication.getName();
7280
String password = (String) authentication.getCredentials();
73-
SpringSecurityLdapTemplate ldapTemplate = new SpringSecurityLdapTemplate(getContextSource());
7481
for (String userDn : getUserDns(username)) {
7582
try {
76-
user = ldapTemplate.retrieveEntry(userDn, getUserAttributes());
83+
user = this.ldapClient.search()
84+
.query(LdapQueryBuilder.query()
85+
.base(userDn)
86+
.searchScope(SearchScope.OBJECT)
87+
.attributes(getUserAttributes()))
88+
.toObject((AttributesMapper<DirContextOperations>) attrs -> {
89+
BaseLdapPathContextSource source = (BaseLdapPathContextSource) getContextSource();
90+
return new DirContextAdapter(attrs, LdapUtils.newLdapName(userDn), source.getBaseLdapName());
91+
});
7792
}
78-
catch (NameNotFoundException ignore) {
93+
catch (EmptyResultDataAccessException ignore) {
7994
logger.trace(LogMessage.format("Failed to retrieve user with %s", userDn), ignore);
8095
}
8196
if (user != null) {
@@ -104,7 +119,7 @@ public DirContextOperations authenticate(final Authentication authentication) {
104119
this.passwordAttributeName, user.getDn()));
105120
return user;
106121
}
107-
if (isLdapPasswordCompare(user, ldapTemplate, password)) {
122+
if (isLdapPasswordCompare(user, password)) {
108123
logger.debug(LogMessage.format("LDAP-matched password attribute '%s' for user '%s'",
109124
this.passwordAttributeName, user.getDn()));
110125
return user;
@@ -129,11 +144,18 @@ private String getPassword(DirContextOperations user) {
129144
return String.valueOf(passwordAttrValue);
130145
}
131146

132-
private boolean isLdapPasswordCompare(DirContextOperations user, SpringSecurityLdapTemplate ldapTemplate,
133-
String password) {
147+
private boolean isLdapPasswordCompare(DirContextOperations user, String password) {
134148
String encodedPassword = this.passwordEncoder.encode(password);
135149
byte[] passwordBytes = Utf8.encode(encodedPassword);
136-
return ldapTemplate.compare(user.getDn().toString(), this.passwordAttributeName, passwordBytes);
150+
return !this.ldapClient.search()
151+
.query(LdapQueryBuilder.query()
152+
.base(user.getDn().toString())
153+
.searchScope(SearchScope.OBJECT)
154+
.countLimit(1)
155+
.attributes(this.passwordAttributeName)
156+
.filter("({0}={1})", this.passwordAttributeName, passwordBytes))
157+
.toList((AttributesMapper<String>) attrs -> this.passwordAttributeName)
158+
.isEmpty();
137159
}
138160

139161
public void setPasswordAttributeName(String passwordAttribute) {

ldap/src/test/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticatorMockTests.java

Lines changed: 0 additions & 60 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law.or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.springframework.security.ldap.authentication;
19+
20+
import java.util.Collections;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.extension.ExtendWith;
23+
import org.mockito.InjectMocks;
24+
import org.mockito.Mock;
25+
import org.mockito.junit.jupiter.MockitoExtension;
26+
27+
import org.springframework.dao.EmptyResultDataAccessException;
28+
import org.springframework.ldap.core.AttributesMapper;
29+
import org.springframework.ldap.core.DirContextOperations;
30+
import org.springframework.ldap.core.LdapClient;
31+
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
32+
import org.springframework.ldap.query.LdapQuery;
33+
import org.springframework.ldap.support.LdapUtils;
34+
import org.springframework.security.authentication.BadCredentialsException;
35+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
36+
import org.springframework.security.core.userdetails.UsernameNotFoundException;
37+
import org.springframework.security.ldap.search.LdapUserSearch;
38+
39+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
40+
import static org.mockito.ArgumentMatchers.any;
41+
import static org.mockito.BDDMockito.given;
42+
import static org.mockito.Mockito.mock;
43+
import static org.mockito.Mockito.verifyNoInteractions;
44+
45+
/**
46+
* Unit tests for {@link PasswordComparisonAuthenticator}.
47+
*
48+
* @author Minkuk Jo
49+
*/
50+
@ExtendWith(MockitoExtension.class)
51+
class PasswordComparisonAuthenticatorUnitTests {
52+
53+
@Mock
54+
BaseLdapPathContextSource contextSource;
55+
56+
@InjectMocks
57+
PasswordComparisonAuthenticator authenticator;
58+
59+
@Mock
60+
LdapClient ldapClient;
61+
62+
@Mock
63+
LdapClient.SearchSpec searchSpec;
64+
65+
@Test
66+
void authenticateWhenUserNotFoundThenThrowsUsernameNotFoundException() {
67+
this.authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" });
68+
this.authenticator.ldapClient = this.ldapClient;
69+
UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken.unauthenticated("user",
70+
"password");
71+
given(this.ldapClient.search()).willReturn(this.searchSpec);
72+
given(this.searchSpec.query(any(LdapQuery.class))).willReturn(this.searchSpec);
73+
given(this.searchSpec.toObject(any(AttributesMapper.class))).willThrow(new EmptyResultDataAccessException(1));
74+
LdapUserSearch userSearch = mock(LdapUserSearch.class);
75+
this.authenticator.setUserSearch(userSearch);
76+
given(userSearch.searchForUser("user")).willReturn(null);
77+
78+
assertThatExceptionOfType(UsernameNotFoundException.class)
79+
.isThrownBy(() -> this.authenticator.authenticate(authentication))
80+
.withMessage("user not found");
81+
verifyNoInteractions(this.contextSource);
82+
}
83+
84+
85+
@Test
86+
void authenticateWhenPasswordCompareFailsThenThrowsBadCredentialsException() {
87+
this.authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" });
88+
this.authenticator.ldapClient = this.ldapClient;
89+
UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken.unauthenticated("user",
90+
"password");
91+
92+
DirContextOperations user = mock(DirContextOperations.class);
93+
LdapClient.SearchSpec userSearchSpec = mock(LdapClient.SearchSpec.class);
94+
given(user.getDn()).willReturn(LdapUtils.newLdapName("uid=user,ou=people"));
95+
given(userSearchSpec.query(any(LdapQuery.class))).willReturn(userSearchSpec);
96+
given(userSearchSpec.toObject(any(AttributesMapper.class))).willReturn(user);
97+
98+
LdapClient.SearchSpec passwordSearchSpec = mock(LdapClient.SearchSpec.class);
99+
given(passwordSearchSpec.query(any(LdapQuery.class))).willReturn(passwordSearchSpec);
100+
given(passwordSearchSpec.toList(any(AttributesMapper.class))).willReturn(Collections.emptyList());
101+
102+
given(this.ldapClient.search()).willReturn(userSearchSpec, passwordSearchSpec);
103+
104+
assertThatExceptionOfType(BadCredentialsException.class)
105+
.isThrownBy(() -> this.authenticator.authenticate(authentication));
106+
}
107+
}

0 commit comments

Comments
 (0)