Improve Error Messages for PasswordEncoder

Closes gh-14880

Signed-off-by: Jonny Coddington <bottlerocketjonny@protonmail.com>
This commit is contained in:
Jonny Coddington
2024-04-26 14:09:28 +01:00
committed by Josh Cummings
parent 2c9c309d7f
commit b90851d968
3 changed files with 73 additions and 14 deletions

View File

@@ -19,6 +19,8 @@ package org.springframework.security.crypto.password;
import java.util.HashMap;
import java.util.Map;
import org.springframework.util.StringUtils;
/**
* A password encoder that delegates to another PasswordEncoder based upon a prefixed
* identifier.
@@ -129,9 +131,14 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
private static final String DEFAULT_ID_SUFFIX = "}";
public static final String NO_PASSWORD_ENCODER_MAPPED = "There is no PasswordEncoder mapped for the id \"%s\"";
private static final String NO_PASSWORD_ENCODER_MAPPED = "There is no password encoder mapped for the id '%s'. "
+ "Check your configuration to ensure it matches one of the registered encoders.";
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}`.";
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. "
+ "Please either prefix this password with '{noop}' or set a default password encoder in `DelegatingPasswordEncoder`.";
private static final String MALFORMED_PASSWORD_ENCODER_PREFIX = "The name of the password encoder is improperly "
+ "formatted or incomplete. The format should be '%sENCODER%spassword'.";
private final String idPrefix;
@@ -290,10 +297,18 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
String id = extractId(prefixEncodedPassword);
if (id != null && !id.isEmpty()) {
if (StringUtils.hasText(id)) {
throw new IllegalArgumentException(String.format(NO_PASSWORD_ENCODER_MAPPED, id));
}
throw new IllegalArgumentException(NO_PASSWORD_ENCODER_PREFIX);
if (StringUtils.hasText(prefixEncodedPassword)) {
int start = prefixEncodedPassword.indexOf(DelegatingPasswordEncoder.this.idPrefix);
int end = prefixEncodedPassword.indexOf(DelegatingPasswordEncoder.this.idSuffix, start);
if (start < 0 && end < 0) {
throw new IllegalArgumentException(NO_PASSWORD_ENCODER_PREFIX);
}
}
throw new IllegalArgumentException(String.format(MALFORMED_PASSWORD_ENCODER_PREFIX,
DelegatingPasswordEncoder.this.idPrefix, DelegatingPasswordEncoder.this.idSuffix));
}
}

View File

@@ -43,8 +43,6 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
@ExtendWith(MockitoExtension.class)
public class DelegatingPasswordEncoderTests {
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}`.";
@Mock
private PasswordEncoder bcrypt;
@@ -70,6 +68,14 @@ public class DelegatingPasswordEncoderTests {
private DelegatingPasswordEncoder onlySuffixPasswordEncoder;
private static final String NO_PASSWORD_ENCODER_MAPPED = "There is no password encoder mapped for the id 'unmapped'. "
+ "Check your configuration to ensure it matches one of the registered encoders.";
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. Please either prefix this password with '{noop}' or set a default password encoder in `DelegatingPasswordEncoder`.";
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'.";
@BeforeEach
public void setup() {
this.delegates = new HashMap<>();
@@ -195,7 +201,7 @@ public class DelegatingPasswordEncoderTests {
public void matchesWhenUnMappedThenIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{unmapped}" + this.rawPassword))
.withMessage("There is no PasswordEncoder mapped for the id \"unmapped\"");
.withMessage(NO_PASSWORD_ENCODER_MAPPED);
verifyNoMoreInteractions(this.bcrypt, this.noop);
}
@@ -203,7 +209,7 @@ public class DelegatingPasswordEncoderTests {
public void matchesWhenNoClosingPrefixStringThenIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{bcrypt" + this.rawPassword))
.withMessage(NO_PASSWORD_ENCODER);
.withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX);
verifyNoMoreInteractions(this.bcrypt, this.noop);
}
@@ -211,7 +217,7 @@ public class DelegatingPasswordEncoderTests {
public void matchesWhenNoStartingPrefixStringThenFalse() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "bcrypt}" + this.rawPassword))
.withMessage(NO_PASSWORD_ENCODER);
.withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX);
verifyNoMoreInteractions(this.bcrypt, this.noop);
}
@@ -219,7 +225,7 @@ public class DelegatingPasswordEncoderTests {
public void matchesWhenNoIdStringThenFalse() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{}" + this.rawPassword))
.withMessage(NO_PASSWORD_ENCODER);
.withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX);
verifyNoMoreInteractions(this.bcrypt, this.noop);
}
@@ -228,7 +234,7 @@ public class DelegatingPasswordEncoderTests {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "invalid" + this.bcryptEncodedPassword))
.isInstanceOf(IllegalArgumentException.class)
.withMessage(NO_PASSWORD_ENCODER);
.withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX);
verifyNoMoreInteractions(this.bcrypt, this.noop);
}
@@ -238,7 +244,7 @@ public class DelegatingPasswordEncoderTests {
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates);
assertThatIllegalArgumentException()
.isThrownBy(() -> passwordEncoder.matches(this.rawPassword, this.rawPassword))
.withMessage(NO_PASSWORD_ENCODER);
.withMessage(NO_PASSWORD_ENCODER_PREFIX);
verifyNoMoreInteractions(this.bcrypt, this.noop);
}
@@ -296,9 +302,8 @@ public class DelegatingPasswordEncoderTests {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.matches("rawPassword", "prefixEncodedPassword"))
.isInstanceOf(IllegalArgumentException.class)
.withMessage(NO_PASSWORD_ENCODER);
.withMessage(NO_PASSWORD_ENCODER_PREFIX);
verifyNoMoreInteractions(this.bcrypt, this.noop);
}
}