Skip to content

Commit 08b0ae0

Browse files
Improved error messages for PasswordEncoder
Issue gh-14880 Signed-off-by: Jonny Coddington <[email protected]>
1 parent 67853d5 commit 08b0ae0

File tree

3 files changed

+71
-14
lines changed

3 files changed

+71
-14
lines changed

core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,24 @@
1818

1919
import java.util.Properties;
2020

21+
import java.util.stream.Stream;
2122
import org.junit.jupiter.api.Test;
2223

24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.Arguments;
26+
import org.junit.jupiter.params.provider.MethodSource;
27+
import org.springframework.security.authentication.AuthenticationManager;
28+
import org.springframework.security.authentication.ProviderManager;
2329
import org.springframework.security.authentication.TestAuthentication;
30+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
31+
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
2432
import org.springframework.security.core.Authentication;
2533
import org.springframework.security.core.context.SecurityContextHolderStrategy;
2634
import org.springframework.security.core.context.SecurityContextImpl;
2735
import org.springframework.security.core.userdetails.PasswordEncodedUser;
2836
import org.springframework.security.core.userdetails.User;
2937
import org.springframework.security.core.userdetails.UserDetails;
38+
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
3039

3140
import static org.assertj.core.api.Assertions.assertThat;
3241
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -97,4 +106,34 @@ public void changePasswordWhenCustomSecurityContextHolderStrategyThenUses() {
97106
verify(strategy).getContext();
98107
}
99108

109+
@ParameterizedTest
110+
@MethodSource("authenticationErrorCases")
111+
void authenticateWhenInvalidMissingOrMalformedIdThenException(String username, String password, String expectedMessage) {
112+
UserDetails user = User.builder()
113+
.username(username)
114+
.password(password)
115+
.roles("USER")
116+
.build();
117+
InMemoryUserDetailsManager userManager = new InMemoryUserDetailsManager(user);
118+
119+
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
120+
authenticationProvider.setUserDetailsService(userManager);
121+
authenticationProvider.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
122+
123+
AuthenticationManager authManager = new ProviderManager(authenticationProvider);
124+
125+
assertThatIllegalArgumentException()
126+
.isThrownBy(() -> authManager.authenticate(new UsernamePasswordAuthenticationToken(username, "password")))
127+
.withMessage(expectedMessage);
128+
}
129+
130+
private static Stream<Arguments> authenticationErrorCases() {
131+
return Stream.of(
132+
Arguments.of("user", "password", "Given that there is no default password encoder configured, each "
133+
+ "password must have a password encoding prefix. Please either prefix this password with '{noop}' or set a default password encoder in `DelegatingPasswordEncoder`."),
134+
Arguments.of("user", "bycrpt}password", "The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'."),
135+
Arguments.of("user", "{bycrptpassword", "The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'."),
136+
Arguments.of("user", "{ren&stimpy}password", "There is no password encoder mapped for the id 'ren&stimpy'. Check your configuration to ensure it matches one of the registered encoders.")
137+
);
138+
}
100139
}

crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.HashMap;
2020
import java.util.Map;
21+
import org.springframework.util.StringUtils;
2122

2223
/**
2324
* A password encoder that delegates to another PasswordEncoder based upon a prefixed
@@ -129,9 +130,14 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
129130

130131
private static final String DEFAULT_ID_SUFFIX = "}";
131132

132-
public static final String NO_PASSWORD_ENCODER_MAPPED = "There is no PasswordEncoder mapped for the id \"%s\"";
133+
private static final String NO_PASSWORD_ENCODER_MAPPED = "There is no password encoder mapped for the id '%s'. "
134+
+ "Check your configuration to ensure it matches one of the registered encoders.";
133135

134-
public static final String NO_PASSWORD_ENCODER_PREFIX = "You have entered a password with no PasswordEncoder. If that is your intent, it should be prefixed with `{noop}`.";
136+
private static final String NO_PASSWORD_ENCODER_PREFIX = "Given that there is no default password encoder configured, each password must have a password encoding prefix. "
137+
+ "Please either prefix this password with '{noop}' or set a default password encoder in `DelegatingPasswordEncoder`.";
138+
139+
private static final String MALFORMED_PASSWORD_ENCODER_PREFIX = "The name of the password encoder is improperly "
140+
+ "formatted or incomplete. The format should be '%sENCODER%spassword'.";
135141

136142
private final String idPrefix;
137143

@@ -290,10 +296,17 @@ public String encode(CharSequence rawPassword) {
290296
@Override
291297
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
292298
String id = extractId(prefixEncodedPassword);
293-
if (id != null && !id.isEmpty()) {
299+
if (StringUtils.hasText(id)) {
294300
throw new IllegalArgumentException(String.format(NO_PASSWORD_ENCODER_MAPPED, id));
295301
}
296-
throw new IllegalArgumentException(NO_PASSWORD_ENCODER_PREFIX);
302+
if (StringUtils.hasText(prefixEncodedPassword)) {
303+
int start = prefixEncodedPassword.indexOf(idPrefix);
304+
int end = prefixEncodedPassword.indexOf(idSuffix, start);
305+
if (start < 0 && end < 0) {
306+
throw new IllegalArgumentException(NO_PASSWORD_ENCODER_PREFIX);
307+
}
308+
}
309+
throw new IllegalArgumentException(String.format(MALFORMED_PASSWORD_ENCODER_PREFIX, idPrefix, idSuffix));
297310
}
298311

299312
}

crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@
4343
@ExtendWith(MockitoExtension.class)
4444
public class DelegatingPasswordEncoderTests {
4545

46-
public static final String NO_PASSWORD_ENCODER = "You have entered a password with no PasswordEncoder. If that is your intent, it should be prefixed with `{noop}`.";
47-
4846
@Mock
4947
private PasswordEncoder bcrypt;
5048

@@ -70,6 +68,14 @@ public class DelegatingPasswordEncoderTests {
7068

7169
private DelegatingPasswordEncoder onlySuffixPasswordEncoder;
7270

71+
private static final String NO_PASSWORD_ENCODER_MAPPED = "There is no password encoder mapped for the id 'unmapped'. "
72+
+ "Check your configuration to ensure it matches one of the registered encoders.";
73+
74+
private static final String NO_PASSWORD_ENCODER_PREFIX = "Given that there is no default password encoder configured, "
75+
+ "each password must have a password encoding prefix. Please either prefix this password with '{noop}' or set a default password encoder in `DelegatingPasswordEncoder`.";
76+
77+
private static final String MALFORMED_PASSWORD_ENCODER_PREFIX = "The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'.";
78+
7379
@BeforeEach
7480
public void setup() {
7581
this.delegates = new HashMap<>();
@@ -195,31 +201,31 @@ public void matchesWhenNoopThenDelegatesToNoop() {
195201
public void matchesWhenUnMappedThenIllegalArgumentException() {
196202
assertThatIllegalArgumentException()
197203
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{unmapped}" + this.rawPassword))
198-
.withMessage("There is no PasswordEncoder mapped for the id \"unmapped\"");
204+
.withMessage(NO_PASSWORD_ENCODER_MAPPED);
199205
verifyNoMoreInteractions(this.bcrypt, this.noop);
200206
}
201207

202208
@Test
203209
public void matchesWhenNoClosingPrefixStringThenIllegalArgumentException() {
204210
assertThatIllegalArgumentException()
205211
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{bcrypt" + this.rawPassword))
206-
.withMessage(NO_PASSWORD_ENCODER);
212+
.withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX);
207213
verifyNoMoreInteractions(this.bcrypt, this.noop);
208214
}
209215

210216
@Test
211217
public void matchesWhenNoStartingPrefixStringThenFalse() {
212218
assertThatIllegalArgumentException()
213219
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "bcrypt}" + this.rawPassword))
214-
.withMessage(NO_PASSWORD_ENCODER);
220+
.withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX);
215221
verifyNoMoreInteractions(this.bcrypt, this.noop);
216222
}
217223

218224
@Test
219225
public void matchesWhenNoIdStringThenFalse() {
220226
assertThatIllegalArgumentException()
221227
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{}" + this.rawPassword))
222-
.withMessage(NO_PASSWORD_ENCODER);
228+
.withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX);
223229
verifyNoMoreInteractions(this.bcrypt, this.noop);
224230
}
225231

@@ -228,7 +234,7 @@ public void matchesWhenPrefixInMiddleThenFalse() {
228234
assertThatIllegalArgumentException()
229235
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "invalid" + this.bcryptEncodedPassword))
230236
.isInstanceOf(IllegalArgumentException.class)
231-
.withMessage(NO_PASSWORD_ENCODER);
237+
.withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX);
232238
verifyNoMoreInteractions(this.bcrypt, this.noop);
233239
}
234240

@@ -238,7 +244,7 @@ public void matchesWhenIdIsNullThenFalse() {
238244
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates);
239245
assertThatIllegalArgumentException()
240246
.isThrownBy(() -> passwordEncoder.matches(this.rawPassword, this.rawPassword))
241-
.withMessage(NO_PASSWORD_ENCODER);
247+
.withMessage(NO_PASSWORD_ENCODER_PREFIX);
242248
verifyNoMoreInteractions(this.bcrypt, this.noop);
243249
}
244250

@@ -296,9 +302,8 @@ void matchesShouldThrowIllegalArgumentExceptionWhenNoPasswordEncoderIsMappedForT
296302
assertThatIllegalArgumentException()
297303
.isThrownBy(() -> this.passwordEncoder.matches("rawPassword", "prefixEncodedPassword"))
298304
.isInstanceOf(IllegalArgumentException.class)
299-
.withMessage(NO_PASSWORD_ENCODER);
305+
.withMessage(NO_PASSWORD_ENCODER_PREFIX);
300306
verifyNoMoreInteractions(this.bcrypt, this.noop);
301-
302307
}
303308

304309
}

0 commit comments

Comments
 (0)