Skip to content

Commit 5cc73a6

Browse files
jzheauxjgrandja
authored andcommitted
Reject Empty Passwords in DirContextAuthenticationStrategy Implementations
A simple bind with a non-empty principal and an empty password is the LDAP unauthenticated bind shape (RFC 4513 5.1.2), which many directories accept as an anonymous bind and report as success. This can cause callers to believe authentication succeeded when no password was actually verified. Add the check to the default strategy and to those that perform a simple bind variant or take a password as an authentication credential, so the rejection is consistent across all entry points (LdapTemplate, LdapClient, direct ContextSource use). Throw AuthenticationException so the caller sees the same exception type as for any other authentication failure. Applications that need a different bind shape can configure a different DirContextAuthenticationStrategy. Closes gh-407 Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
1 parent 67b9475 commit 5cc73a6

9 files changed

Lines changed: 190 additions & 0 deletions

core/src/main/java/org/springframework/ldap/core/support/DefaultTlsDirContextAuthenticationStrategy.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
import javax.naming.NamingException;
2121
import javax.naming.ldap.LdapContext;
2222

23+
import org.springframework.ldap.AuthenticationException;
24+
import org.springframework.util.StringUtils;
25+
2326
/**
2427
* Default implementation of TLS authentication. Applies <code>SIMPLE</code>
2528
* authentication on top of the negotiated TLS session. Refer to
@@ -34,6 +37,10 @@ public class DefaultTlsDirContextAuthenticationStrategy extends AbstractTlsDirCo
3437
private static final String SIMPLE_AUTHENTICATION = "simple";
3538

3639
protected void applyAuthentication(LdapContext ctx, String userDn, String password) throws NamingException {
40+
if (StringUtils.hasLength(userDn) && !StringUtils.hasLength(password)) {
41+
throw new AuthenticationException(
42+
new javax.naming.AuthenticationException("password must be provided when userDn is set"));
43+
}
3744
ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, SIMPLE_AUTHENTICATION);
3845
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userDn);
3946
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);

core/src/main/java/org/springframework/ldap/core/support/DigestMd5DirContextAuthenticationStrategy.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
import javax.naming.Context;
2222
import javax.naming.directory.DirContext;
2323

24+
import org.springframework.ldap.AuthenticationException;
25+
import org.springframework.util.StringUtils;
26+
2427
/**
2528
* Authentication strategy for LDAP DIGEST-MD5 SASL mechanism.
2629
*
@@ -50,6 +53,10 @@ public DirContext processContextAfterCreation(DirContext ctx, String userDn, Str
5053
* setupEnvironment(java.util.Hashtable, java.lang.String, java.lang.String)
5154
*/
5255
public void setupEnvironment(Hashtable<String, Object> env, String userDn, String password) {
56+
if (StringUtils.hasLength(userDn) && !StringUtils.hasLength(password)) {
57+
throw new AuthenticationException(
58+
new javax.naming.AuthenticationException("password must be provided when userDn is set"));
59+
}
5360
env.put(Context.SECURITY_AUTHENTICATION, DIGEST_MD5_AUTHENTICATION);
5461
// userDn should be a bare username for DIGEST-MD5
5562
env.put(Context.SECURITY_PRINCIPAL, userDn);

core/src/main/java/org/springframework/ldap/core/support/SimpleDirContextAuthenticationStrategy.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
import javax.naming.Context;
2222
import javax.naming.directory.DirContext;
2323

24+
import org.springframework.ldap.AuthenticationException;
25+
import org.springframework.util.StringUtils;
26+
2427
/**
2528
* The default {@link DirContextAuthenticationStrategy} implementation, setting the
2629
* <code>DirContext</code> environment up for 'SIMPLE' authentication, and specifying the
@@ -40,6 +43,10 @@ public class SimpleDirContextAuthenticationStrategy implements DirContextAuthent
4043
* setupEnvironment(java.util.Hashtable, java.lang.String, java.lang.String)
4144
*/
4245
public void setupEnvironment(Hashtable<String, Object> env, String userDn, String password) {
46+
if (StringUtils.hasLength(userDn) && !StringUtils.hasLength(password)) {
47+
throw new AuthenticationException(
48+
new javax.naming.AuthenticationException("password must be provided when userDn is set"));
49+
}
4350
env.put(Context.SECURITY_AUTHENTICATION, SIMPLE_AUTHENTICATION);
4451
env.put(Context.SECURITY_PRINCIPAL, userDn);
4552
env.put(Context.SECURITY_CREDENTIALS, password);

core/src/test/java/org/springframework/ldap/core/DefaultLdapClientTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
import org.springframework.dao.EmptyResultDataAccessException;
3838
import org.springframework.dao.IncorrectResultSizeDataAccessException;
39+
import org.springframework.ldap.AuthenticationException;
3940
import org.springframework.ldap.LimitExceededException;
4041
import org.springframework.ldap.NameNotFoundException;
4142
import org.springframework.ldap.PartialResultException;
@@ -660,6 +661,26 @@ public void createWhenLdapTemplateThenUses() {
660661
verify(ldap).getDefaultTimeLimit();
661662
}
662663

664+
// gh-407
665+
@Test
666+
public void authenticateWhenPasswordIsEmptyAndDefaultStrategyThenAuthenticationException() throws Exception {
667+
given(this.contextSourceMock.getReadOnlyContext()).willReturn(this.dirContextMock);
668+
669+
Object expectedObject = new DirContextAdapter(new BasicAttributes(), LdapUtils.newLdapName("cn=john doe"),
670+
LdapUtils.newLdapName("dc=jayway, dc=se"));
671+
SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());
672+
singleSearchResult(searchControlsRecursive(), searchResult);
673+
674+
given(this.contextSourceMock.getContext("cn=john doe,dc=jayway,dc=se", ""))
675+
.willThrow(new AuthenticationException(
676+
new javax.naming.AuthenticationException("password must be provided when userDn is set")));
677+
678+
LdapQuery query = LdapQueryBuilder.query().base(this.nameMock).filter("(ou=somevalue)");
679+
assertThatExceptionOfType(AuthenticationException.class)
680+
.isThrownBy(() -> this.tested.authenticate().query(query).password("").execute());
681+
verify(this.dirContextMock).close();
682+
}
683+
663684
private void noSearchResults(SearchControls controls) throws Exception {
664685
given(this.dirContextMock.search(eq(this.nameMock), eq("(ou=somevalue)"),
665686
argThat(new SearchControlsMatcher(controls))))

core/src/test/java/org/springframework/ldap/core/LdapTemplateTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.LdapDataEntry;
4141
import org.springframework.dao.EmptyResultDataAccessException;
4242
import org.springframework.dao.IncorrectResultSizeDataAccessException;
43+
import org.springframework.ldap.AuthenticationException;
4344
import org.springframework.ldap.LimitExceededException;
4445
import org.springframework.ldap.NameNotFoundException;
4546
import org.springframework.ldap.PartialResultException;
@@ -1880,6 +1881,26 @@ public void testAuthenticateWithErrorInCallbackShouldFail() throws Exception {
18801881
assertThat(result).isFalse();
18811882
}
18821883

1884+
// gh-407
1885+
@Test
1886+
public void authenticateWhenPasswordIsEmptyAndDefaultStrategyThenReturnsFalse() throws Exception {
1887+
given(this.contextSourceMock.getReadOnlyContext()).willReturn(this.dirContextMock);
1888+
1889+
Object expectedObject = new DirContextAdapter(new BasicAttributes(), LdapUtils.newLdapName("cn=john doe"),
1890+
LdapUtils.newLdapName("dc=jayway, dc=se"));
1891+
SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());
1892+
singleSearchResult(searchControlsRecursive(), searchResult);
1893+
1894+
given(this.contextSourceMock.getContext("cn=john doe,dc=jayway,dc=se", ""))
1895+
.willThrow(new AuthenticationException(
1896+
new javax.naming.AuthenticationException("password must be provided when userDn is set")));
1897+
1898+
boolean result = this.tested.authenticate(this.nameMock, "(ou=somevalue)", "", this.entryContextCallbackMock);
1899+
1900+
assertThat(result).isFalse();
1901+
verify(this.dirContextMock).close();
1902+
}
1903+
18831904
private void noSearchResults(SearchControls controls) throws Exception {
18841905
given(this.dirContextMock.search(eq(this.nameMock), eq("(ou=somevalue)"),
18851906
argThat(new SearchControlsMatcher(controls))))

core/src/test/java/org/springframework/ldap/core/support/DefaultTlsDirContextAuthenticationStrategyTests.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
import org.mockito.Mock;
2424
import org.mockito.junit.MockitoJUnitRunner;
2525

26+
import org.springframework.ldap.AuthenticationException;
27+
28+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2629
import static org.mockito.Mockito.verify;
2730

2831
/**
@@ -45,4 +48,16 @@ public void applyAuthenticationThenLookupInvoked() throws Exception {
4548
verify(this.context).lookup("");
4649
}
4750

51+
@Test
52+
public void applyAuthenticationWhenUserDnIsSetAndPasswordIsEmptyThenAuthenticationException() {
53+
assertThatExceptionOfType(AuthenticationException.class)
54+
.isThrownBy(() -> this.strategy.applyAuthentication(this.context, "username", ""));
55+
}
56+
57+
@Test
58+
public void applyAuthenticationWhenUserDnIsSetAndPasswordIsNullThenAuthenticationException() {
59+
assertThatExceptionOfType(AuthenticationException.class)
60+
.isThrownBy(() -> this.strategy.applyAuthentication(this.context, "username", null));
61+
}
62+
4863
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2006-present 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 obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ldap.core.support;
18+
19+
import java.util.Hashtable;
20+
21+
import javax.naming.Context;
22+
23+
import org.junit.Before;
24+
import org.junit.Test;
25+
26+
import org.springframework.ldap.AuthenticationException;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
30+
31+
public class DigestMd5DirContextAuthenticationStrategyTests {
32+
33+
private DigestMd5DirContextAuthenticationStrategy tested;
34+
35+
@Before
36+
public void setUp() {
37+
this.tested = new DigestMd5DirContextAuthenticationStrategy();
38+
}
39+
40+
@Test
41+
public void setupEnvironmentSetsAuthenticationPrincipalAndCredentials() {
42+
Hashtable<String, Object> env = new Hashtable<>();
43+
this.tested.setupEnvironment(env, "username", "pw");
44+
45+
assertThat(env.get(Context.SECURITY_AUTHENTICATION)).isEqualTo("DIGEST-MD5");
46+
assertThat(env.get(Context.SECURITY_PRINCIPAL)).isEqualTo("username");
47+
assertThat(env.get(Context.SECURITY_CREDENTIALS)).isEqualTo("pw");
48+
}
49+
50+
@Test
51+
public void setupEnvironmentWhenUserDnIsSetAndPasswordIsEmptyThenAuthenticationException() {
52+
Hashtable<String, Object> env = new Hashtable<>();
53+
assertThatExceptionOfType(AuthenticationException.class)
54+
.isThrownBy(() -> this.tested.setupEnvironment(env, "username", ""));
55+
}
56+
57+
@Test
58+
public void setupEnvironmentWhenUserDnIsSetAndPasswordIsNullThenAuthenticationException() {
59+
Hashtable<String, Object> env = new Hashtable<>();
60+
assertThatExceptionOfType(AuthenticationException.class)
61+
.isThrownBy(() -> this.tested.setupEnvironment(env, "username", null));
62+
}
63+
64+
}

core/src/test/java/org/springframework/ldap/core/support/LdapContextSourceTests.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
import org.junit.Before;
2525
import org.junit.Test;
2626

27+
import org.springframework.ldap.AuthenticationException;
2728
import org.springframework.ldap.support.LdapUtils;
2829

2930
import static org.assertj.core.api.Assertions.assertThat;
31+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
3032

3133
/**
3234
* Unit tests for the LdapContextSource class.
@@ -158,6 +160,26 @@ public void testGetAuthenticatedEnv() throws Exception {
158160
.isEqualTo(LdapUtils.newLdapName("dc=example,dc=se"));
159161
}
160162

163+
@Test
164+
public void getContextWhenUserDnIsSetAndPasswordIsEmptyThenAuthenticationException() {
165+
this.tested.setUrl("ldap://ldap.example.com:389");
166+
this.tested.setUserDn("cn=service");
167+
this.tested.setPassword("secret");
168+
this.tested.afterPropertiesSet();
169+
assertThatExceptionOfType(AuthenticationException.class)
170+
.isThrownBy(() -> this.tested.getContext("cn=admin", ""));
171+
}
172+
173+
@Test
174+
public void getContextWhenUserDnIsSetAndPasswordIsNullThenAuthenticationException() {
175+
this.tested.setUrl("ldap://ldap.example.com:389");
176+
this.tested.setUserDn("cn=service");
177+
this.tested.setPassword("secret");
178+
this.tested.afterPropertiesSet();
179+
assertThatExceptionOfType(AuthenticationException.class)
180+
.isThrownBy(() -> this.tested.getContext("cn=admin", null));
181+
}
182+
161183
@Test
162184
public void testGetAnonymousEnvWhenCacheIsOff() throws Exception {
163185
this.tested.setBase("dc=example,dc=se");

core/src/test/java/org/springframework/ldap/core/support/SimpleDirContextAuthenticationStrategyTests.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
import org.junit.Before;
2424
import org.junit.Test;
2525

26+
import org.springframework.ldap.AuthenticationException;
27+
2628
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2730

2831
public class SimpleDirContextAuthenticationStrategyTests {
2932

@@ -52,4 +55,27 @@ public void testProcessContextAfterCreation() {
5255
assertThat(env.isEmpty()).isTrue();
5356
}
5457

58+
@Test
59+
public void setupEnvironmentWhenUserDnIsSetAndPasswordIsEmptyThenAuthenticationException() {
60+
Hashtable<String, Object> env = new Hashtable<>();
61+
assertThatExceptionOfType(AuthenticationException.class)
62+
.isThrownBy(() -> this.tested.setupEnvironment(env, "cn=John Doe", ""));
63+
}
64+
65+
@Test
66+
public void setupEnvironmentWhenUserDnIsSetAndPasswordIsNullThenAuthenticationException() {
67+
Hashtable<String, Object> env = new Hashtable<>();
68+
assertThatExceptionOfType(AuthenticationException.class)
69+
.isThrownBy(() -> this.tested.setupEnvironment(env, "cn=John Doe", null));
70+
}
71+
72+
@Test
73+
public void setupEnvironmentWhenUserDnAndPasswordAreEmptyThenSucceeds() {
74+
Hashtable<String, Object> env = new Hashtable<>();
75+
this.tested.setupEnvironment(env, "", "");
76+
assertThat(env.get(Context.SECURITY_AUTHENTICATION)).isEqualTo("simple");
77+
assertThat(env.get(Context.SECURITY_PRINCIPAL)).isEqualTo("");
78+
assertThat(env.get(Context.SECURITY_CREDENTIALS)).isEqualTo("");
79+
}
80+
5581
}

0 commit comments

Comments
 (0)