Skip to content

Commit b90851d

Browse files
bottlerocketjonnyjzheaux
authored andcommitted
Improve Error Messages for PasswordEncoder
Closes gh-14880 Signed-off-by: Jonny Coddington <[email protected]>
1 parent 2c9c309 commit b90851d

File tree

3 files changed

+73
-14
lines changed

3 files changed

+73
-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,10 +18,18 @@
1818

1919
import java.util.Collection;
2020
import java.util.Properties;
21+
import java.util.stream.Stream;
2122

2223
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.Arguments;
26+
import org.junit.jupiter.params.provider.MethodSource;
2327

28+
import org.springframework.security.authentication.AuthenticationManager;
29+
import org.springframework.security.authentication.ProviderManager;
2430
import org.springframework.security.authentication.TestAuthentication;
31+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
32+
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
2533
import org.springframework.security.core.Authentication;
2634
import org.springframework.security.core.CredentialsContainer;
2735
import org.springframework.security.core.GrantedAuthority;
@@ -31,6 +39,7 @@
3139
import org.springframework.security.core.userdetails.User;
3240
import org.springframework.security.core.userdetails.UserDetails;
3341
import org.springframework.security.core.userdetails.UsernameNotFoundException;
42+
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
3443

3544
import static org.assertj.core.api.Assertions.assertThat;
3645
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -151,6 +160,36 @@ public void loadUserByUsernameWhenInstanceOfCredentialsContainerThenReturnInstan
151160
assertThat(manager.loadUserByUsername(user.getUsername())).isSameAs(user);
152161
}
153162

163+
@ParameterizedTest
164+
@MethodSource("authenticationErrorCases")
165+
void authenticateWhenInvalidMissingOrMalformedIdThenException(String username, String password,
166+
String expectedMessage) {
167+
UserDetails user = User.builder().username(username).password(password).roles("USER").build();
168+
InMemoryUserDetailsManager userManager = new InMemoryUserDetailsManager(user);
169+
170+
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
171+
authenticationProvider.setUserDetailsService(userManager);
172+
authenticationProvider.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
173+
174+
AuthenticationManager authManager = new ProviderManager(authenticationProvider);
175+
176+
assertThatIllegalArgumentException()
177+
.isThrownBy(() -> authManager.authenticate(new UsernamePasswordAuthenticationToken(username, "password")))
178+
.withMessage(expectedMessage);
179+
}
180+
181+
private static Stream<Arguments> authenticationErrorCases() {
182+
return Stream.of(Arguments
183+
.of("user", "password", "Given that there is no default password encoder configured, each "
184+
+ "password must have a password encoding prefix. Please either prefix this password with '{noop}' or set a default password encoder in `DelegatingPasswordEncoder`."),
185+
Arguments.of("user", "bycrpt}password",
186+
"The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'."),
187+
Arguments.of("user", "{bycrptpassword",
188+
"The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'."),
189+
Arguments.of("user", "{ren&stimpy}password",
190+
"There is no password encoder mapped for the id 'ren&stimpy'. Check your configuration to ensure it matches one of the registered encoders."));
191+
}
192+
154193
static class CustomUser implements MutableUserDetails, CredentialsContainer {
155194

156195
private final UserDetails delegate;

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.util.HashMap;
2020
import java.util.Map;
2121

22+
import org.springframework.util.StringUtils;
23+
2224
/**
2325
* A password encoder that delegates to another PasswordEncoder based upon a prefixed
2426
* identifier.
@@ -129,9 +131,14 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
129131

130132
private static final String DEFAULT_ID_SUFFIX = "}";
131133

132-
public static final String NO_PASSWORD_ENCODER_MAPPED = "There is no PasswordEncoder mapped for the id \"%s\"";
134+
private static final String NO_PASSWORD_ENCODER_MAPPED = "There is no password encoder mapped for the id '%s'. "
135+
+ "Check your configuration to ensure it matches one of the registered encoders.";
136+
137+
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. "
138+
+ "Please either prefix this password with '{noop}' or set a default password encoder in `DelegatingPasswordEncoder`.";
133139

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}`.";
140+
private static final String MALFORMED_PASSWORD_ENCODER_PREFIX = "The name of the password encoder is improperly "
141+
+ "formatted or incomplete. The format should be '%sENCODER%spassword'.";
135142

136143
private final String idPrefix;
137144

@@ -290,10 +297,18 @@ public String encode(CharSequence rawPassword) {
290297
@Override
291298
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
292299
String id = extractId(prefixEncodedPassword);
293-
if (id != null && !id.isEmpty()) {
300+
if (StringUtils.hasText(id)) {
294301
throw new IllegalArgumentException(String.format(NO_PASSWORD_ENCODER_MAPPED, id));
295302
}
296-
throw new IllegalArgumentException(NO_PASSWORD_ENCODER_PREFIX);
303+
if (StringUtils.hasText(prefixEncodedPassword)) {
304+
int start = prefixEncodedPassword.indexOf(DelegatingPasswordEncoder.this.idPrefix);
305+
int end = prefixEncodedPassword.indexOf(DelegatingPasswordEncoder.this.idSuffix, start);
306+
if (start < 0 && end < 0) {
307+
throw new IllegalArgumentException(NO_PASSWORD_ENCODER_PREFIX);
308+
}
309+
}
310+
throw new IllegalArgumentException(String.format(MALFORMED_PASSWORD_ENCODER_PREFIX,
311+
DelegatingPasswordEncoder.this.idPrefix, DelegatingPasswordEncoder.this.idSuffix));
297312
}
298313

299314
}

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)