Skip to content

Improved error message for PasswordEncoder #14968

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,18 @@

import java.util.Collection;
import java.util.Properties;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.TestAuthentication;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority;
Expand All @@ -31,6 +39,7 @@
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
Expand Down Expand Up @@ -151,6 +160,36 @@ public void loadUserByUsernameWhenInstanceOfCredentialsContainerThenReturnInstan
assertThat(manager.loadUserByUsername(user.getUsername())).isSameAs(user);
}

@ParameterizedTest
@MethodSource("authenticationErrorCases")
void authenticateWhenInvalidMissingOrMalformedIdThenException(String username, String password,
String expectedMessage) {
UserDetails user = User.builder().username(username).password(password).roles("USER").build();
InMemoryUserDetailsManager userManager = new InMemoryUserDetailsManager(user);

DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userManager);
authenticationProvider.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());

AuthenticationManager authManager = new ProviderManager(authenticationProvider);

assertThatIllegalArgumentException()
.isThrownBy(() -> authManager.authenticate(new UsernamePasswordAuthenticationToken(username, "password")))
.withMessage(expectedMessage);
}

private static Stream<Arguments> authenticationErrorCases() {
return Stream.of(Arguments
.of("user", "password", "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`."),
Arguments.of("user", "bycrpt}password",
"The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'."),
Arguments.of("user", "{bycrptpassword",
"The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'."),
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."));
}

static class CustomUser implements MutableUserDetails, CredentialsContainer {

private final UserDetails delegate;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
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.
Expand Down Expand Up @@ -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.";

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`.";

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 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;

Expand Down Expand Up @@ -290,10 +297,18 @@ public String encode(CharSequence rawPassword) {
@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));
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@
@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;

Expand All @@ -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<>();
Expand Down Expand Up @@ -195,31 +201,31 @@ public void matchesWhenNoopThenDelegatesToNoop() {
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);
}

@Test
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);
}

@Test
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);
}

@Test
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);
}

Expand All @@ -228,7 +234,7 @@ public void matchesWhenPrefixInMiddleThenFalse() {
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);
}

Expand All @@ -238,7 +244,7 @@ public void matchesWhenIdIsNullThenFalse() {
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);
}

Expand Down Expand Up @@ -296,9 +302,8 @@ void matchesShouldThrowIllegalArgumentExceptionWhenNoPasswordEncoderIsMappedForT
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.matches("rawPassword", "prefixEncodedPassword"))
.isInstanceOf(IllegalArgumentException.class)
.withMessage(NO_PASSWORD_ENCODER);
.withMessage(NO_PASSWORD_ENCODER_PREFIX);
verifyNoMoreInteractions(this.bcrypt, this.noop);

}

}