Skip to content

Commit f5f87cf

Browse files
Improved error message for PasswordEncoder
Signed-off-by: Jonny Coddington <[email protected]>
1 parent 499c920 commit f5f87cf

File tree

3 files changed

+58
-11
lines changed

3 files changed

+58
-11
lines changed

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

Lines changed: 38 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,33 @@ 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", "Password encoding information cannot be null. If no encoding is intended, ensure it is prefixed with '{noop}'."),
133+
Arguments.of("user", "bycrpt}password", "The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'."),
134+
Arguments.of("user", "{bycrptpassword", "The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'."),
135+
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.")
136+
);
137+
}
100138
}

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,12 @@ private String extractId(String prefixEncodedPassword) {
245245
return null;
246246
}
247247
int start = prefixEncodedPassword.indexOf(this.idPrefix);
248-
if (start != 0) {
248+
if (start == -1 && !prefixEncodedPassword.contains(this.idSuffix)) {
249249
return null;
250250
}
251-
int end = prefixEncodedPassword.indexOf(this.idSuffix, start);
252-
if (end < 0) {
253-
return null;
251+
int end = prefixEncodedPassword.indexOf(this.idSuffix, start + this.idPrefix.length());
252+
if (start != 0 || end == -1) {
253+
return "";
254254
}
255255
return prefixEncodedPassword.substring(start + this.idPrefix.length(), end);
256256
}
@@ -286,7 +286,16 @@ public String encode(CharSequence rawPassword) {
286286
@Override
287287
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
288288
String id = extractId(prefixEncodedPassword);
289-
throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
289+
if (id == null) {
290+
throw new IllegalArgumentException("Password encoding information cannot be null. " +
291+
"If no encoding is intended, ensure it is prefixed with '{noop}'.");
292+
} else if (id.isEmpty()) {
293+
throw new IllegalArgumentException(String.format("The name of the password encoder is improperly "
294+
+ "formatted or incomplete. The format should be '%sENCODER%spassword'.", idPrefix, idSuffix));
295+
} else {
296+
throw new IllegalArgumentException(String.format("There is no password encoder mapped for the id '%s'. " +
297+
"Check your configuration to ensure it matches one of the registered encoders.", id));
298+
}
290299
}
291300

292301
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,31 +193,31 @@ public void matchesWhenNoopThenDelegatesToNoop() {
193193
public void matchesWhenUnMappedThenIllegalArgumentException() {
194194
assertThatIllegalArgumentException()
195195
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{unmapped}" + this.rawPassword))
196-
.withMessage("There is no PasswordEncoder mapped for the id \"unmapped\"");
196+
.withMessage("There is no password encoder mapped for the id 'unmapped'. Check your configuration to ensure it matches one of the registered encoders.");
197197
verifyNoMoreInteractions(this.bcrypt, this.noop);
198198
}
199199

200200
@Test
201201
public void matchesWhenNoClosingPrefixStringThenIllegalArgumentException() {
202202
assertThatIllegalArgumentException()
203203
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{bcrypt" + this.rawPassword))
204-
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
204+
.withMessage("The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'.");
205205
verifyNoMoreInteractions(this.bcrypt, this.noop);
206206
}
207207

208208
@Test
209209
public void matchesWhenNoStartingPrefixStringThenFalse() {
210210
assertThatIllegalArgumentException()
211211
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "bcrypt}" + this.rawPassword))
212-
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
212+
.withMessage("The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'.");
213213
verifyNoMoreInteractions(this.bcrypt, this.noop);
214214
}
215215

216216
@Test
217217
public void matchesWhenNoIdStringThenFalse() {
218218
assertThatIllegalArgumentException()
219219
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{}" + this.rawPassword))
220-
.withMessage("There is no PasswordEncoder mapped for the id \"\"");
220+
.withMessage("The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'.");
221221
verifyNoMoreInteractions(this.bcrypt, this.noop);
222222
}
223223

@@ -226,7 +226,7 @@ public void matchesWhenPrefixInMiddleThenFalse() {
226226
assertThatIllegalArgumentException()
227227
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "invalid" + this.bcryptEncodedPassword))
228228
.isInstanceOf(IllegalArgumentException.class)
229-
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
229+
.withMessage("The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'.");
230230
verifyNoMoreInteractions(this.bcrypt, this.noop);
231231
}
232232

@@ -236,7 +236,7 @@ public void matchesWhenIdIsNullThenFalse() {
236236
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates);
237237
assertThatIllegalArgumentException()
238238
.isThrownBy(() -> passwordEncoder.matches(this.rawPassword, this.rawPassword))
239-
.withMessage("There is no PasswordEncoder mapped for the id \"null\"");
239+
.withMessage("Password encoding information cannot be null. If no encoding is intended, ensure it is prefixed with '{noop}'.");
240240
verifyNoMoreInteractions(this.bcrypt, this.noop);
241241
}
242242

0 commit comments

Comments
 (0)