Support for changing prefix and suffix in `DelegatingPasswordEncoder`
Closes gh-10273
This commit is contained in:
		
							parent
							
								
									ec8912aa47
								
							
						
					
					
						commit
						399cf2e59d
					
				| 
						 | 
					@ -116,14 +116,19 @@ import java.util.Map;
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @author Rob Winch
 | 
					 * @author Rob Winch
 | 
				
			||||||
 * @author Michael Simons
 | 
					 * @author Michael Simons
 | 
				
			||||||
 | 
					 * @author heowc
 | 
				
			||||||
 * @since 5.0
 | 
					 * @since 5.0
 | 
				
			||||||
 * @see org.springframework.security.crypto.factory.PasswordEncoderFactories
 | 
					 * @see org.springframework.security.crypto.factory.PasswordEncoderFactories
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
public class DelegatingPasswordEncoder implements PasswordEncoder {
 | 
					public class DelegatingPasswordEncoder implements PasswordEncoder {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private static final String PREFIX = "{";
 | 
						private static final String DEFAULT_PREFIX = "{";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private static final String SUFFIX = "}";
 | 
						private static final String DEFAULT_SUFFIX = "}";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private final String prefix;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private final String suffix;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private final String idForEncode;
 | 
						private final String idForEncode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -142,9 +147,31 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
 | 
				
			||||||
	 * {@link #matches(CharSequence, String)}
 | 
						 * {@link #matches(CharSequence, String)}
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
 | 
						public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
 | 
				
			||||||
 | 
							this(idForEncode, idToPasswordEncoder, DEFAULT_PREFIX, DEFAULT_SUFFIX);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Creates a new instance
 | 
				
			||||||
 | 
						 * @param idForEncode the id used to lookup which {@link PasswordEncoder} should be
 | 
				
			||||||
 | 
						 * used for {@link #encode(CharSequence)}
 | 
				
			||||||
 | 
						 * @param idToPasswordEncoder a Map of id to {@link PasswordEncoder} used to determine
 | 
				
			||||||
 | 
						 * which {@link PasswordEncoder} should be used for
 | 
				
			||||||
 | 
						 * @param prefix the prefix that denotes the start of an {@code idForEncode}
 | 
				
			||||||
 | 
						 * @param suffix the suffix that denotes the end of an {@code idForEncode}
 | 
				
			||||||
 | 
						 * {@link #matches(CharSequence, String)}
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder,
 | 
				
			||||||
 | 
								String prefix, String suffix) {
 | 
				
			||||||
		if (idForEncode == null) {
 | 
							if (idForEncode == null) {
 | 
				
			||||||
			throw new IllegalArgumentException("idForEncode cannot be null");
 | 
								throw new IllegalArgumentException("idForEncode cannot be null");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							if (prefix == null) {
 | 
				
			||||||
 | 
								throw new IllegalArgumentException("prefix cannot be null");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (suffix == null || suffix.isEmpty()) {
 | 
				
			||||||
 | 
								throw new IllegalArgumentException("suffix cannot be empty");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!idToPasswordEncoder.containsKey(idForEncode)) {
 | 
							if (!idToPasswordEncoder.containsKey(idForEncode)) {
 | 
				
			||||||
			throw new IllegalArgumentException(
 | 
								throw new IllegalArgumentException(
 | 
				
			||||||
					"idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
 | 
										"idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
 | 
				
			||||||
| 
						 | 
					@ -153,16 +180,18 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
 | 
				
			||||||
			if (id == null) {
 | 
								if (id == null) {
 | 
				
			||||||
				continue;
 | 
									continue;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if (id.contains(PREFIX)) {
 | 
								if (!prefix.isEmpty() && id.contains(prefix)) {
 | 
				
			||||||
				throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);
 | 
									throw new IllegalArgumentException("id " + id + " cannot contain " + prefix);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if (id.contains(SUFFIX)) {
 | 
								if (id.contains(suffix)) {
 | 
				
			||||||
				throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);
 | 
									throw new IllegalArgumentException("id " + id + " cannot contain " + suffix);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		this.idForEncode = idForEncode;
 | 
							this.idForEncode = idForEncode;
 | 
				
			||||||
		this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
 | 
							this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
 | 
				
			||||||
		this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
 | 
							this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
 | 
				
			||||||
 | 
							this.prefix = prefix;
 | 
				
			||||||
 | 
							this.suffix = suffix;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
| 
						 | 
					@ -188,7 +217,7 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Override
 | 
						@Override
 | 
				
			||||||
	public String encode(CharSequence rawPassword) {
 | 
						public String encode(CharSequence rawPassword) {
 | 
				
			||||||
		return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
 | 
							return this.prefix + this.idForEncode + this.suffix + this.passwordEncoderForEncode.encode(rawPassword);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Override
 | 
						@Override
 | 
				
			||||||
| 
						 | 
					@ -209,15 +238,15 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
 | 
				
			||||||
		if (prefixEncodedPassword == null) {
 | 
							if (prefixEncodedPassword == null) {
 | 
				
			||||||
			return null;
 | 
								return null;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		int start = prefixEncodedPassword.indexOf(PREFIX);
 | 
							int start = prefixEncodedPassword.indexOf(this.prefix);
 | 
				
			||||||
		if (start != 0) {
 | 
							if (start != 0) {
 | 
				
			||||||
			return null;
 | 
								return null;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		int end = prefixEncodedPassword.indexOf(SUFFIX, start);
 | 
							int end = prefixEncodedPassword.indexOf(this.suffix, start);
 | 
				
			||||||
		if (end < 0) {
 | 
							if (end < 0) {
 | 
				
			||||||
			return null;
 | 
								return null;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return prefixEncodedPassword.substring(start + 1, end);
 | 
							return prefixEncodedPassword.substring(start + this.prefix.length(), end);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Override
 | 
						@Override
 | 
				
			||||||
| 
						 | 
					@ -233,8 +262,8 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private String extractEncodedPassword(String prefixEncodedPassword) {
 | 
						private String extractEncodedPassword(String prefixEncodedPassword) {
 | 
				
			||||||
		int start = prefixEncodedPassword.indexOf(SUFFIX);
 | 
							int start = prefixEncodedPassword.indexOf(this.suffix);
 | 
				
			||||||
		return prefixEncodedPassword.substring(start + 1);
 | 
							return prefixEncodedPassword.substring(start + this.suffix.length());
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,6 +36,7 @@ import static org.mockito.Mockito.verifyZeroInteractions;
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @author Rob Winch
 | 
					 * @author Rob Winch
 | 
				
			||||||
 * @author Michael Simons
 | 
					 * @author Michael Simons
 | 
				
			||||||
 | 
					 * @author heowc
 | 
				
			||||||
 * @since 5.0
 | 
					 * @since 5.0
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@ExtendWith(MockitoExtension.class)
 | 
					@ExtendWith(MockitoExtension.class)
 | 
				
			||||||
| 
						 | 
					@ -64,12 +65,16 @@ public class DelegatingPasswordEncoderTests {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private DelegatingPasswordEncoder passwordEncoder;
 | 
						private DelegatingPasswordEncoder passwordEncoder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private DelegatingPasswordEncoder onlySuffixPasswordEncoder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@BeforeEach
 | 
						@BeforeEach
 | 
				
			||||||
	public void setup() {
 | 
						public void setup() {
 | 
				
			||||||
		this.delegates = new HashMap<>();
 | 
							this.delegates = new HashMap<>();
 | 
				
			||||||
		this.delegates.put(this.bcryptId, this.bcrypt);
 | 
							this.delegates.put(this.bcryptId, this.bcrypt);
 | 
				
			||||||
		this.delegates.put("noop", this.noop);
 | 
							this.delegates.put("noop", this.noop);
 | 
				
			||||||
		this.passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates);
 | 
							this.passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.onlySuffixPasswordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "", "$");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Test
 | 
						@Test
 | 
				
			||||||
| 
						 | 
					@ -83,6 +88,49 @@ public class DelegatingPasswordEncoderTests {
 | 
				
			||||||
				.isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId + "INVALID", this.delegates));
 | 
									.isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId + "INVALID", this.delegates));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Test
 | 
				
			||||||
 | 
						public void constructorWhenPrefixIsNull() {
 | 
				
			||||||
 | 
							assertThatIllegalArgumentException()
 | 
				
			||||||
 | 
									.isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, null, "$"));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Test
 | 
				
			||||||
 | 
						public void constructorWhenSuffixIsNull() {
 | 
				
			||||||
 | 
							assertThatIllegalArgumentException()
 | 
				
			||||||
 | 
									.isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "$", null));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Test
 | 
				
			||||||
 | 
						public void constructorWhenPrefixIsEmpty() {
 | 
				
			||||||
 | 
							assertThat(new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "", "$")).isNotNull();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Test
 | 
				
			||||||
 | 
						public void constructorWhenSuffixIsEmpty() {
 | 
				
			||||||
 | 
							assertThatIllegalArgumentException()
 | 
				
			||||||
 | 
									.isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "$", ""));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Test
 | 
				
			||||||
 | 
						public void constructorWhenPrefixAndSuffixAreEmpty() {
 | 
				
			||||||
 | 
							assertThatIllegalArgumentException()
 | 
				
			||||||
 | 
									.isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "", ""));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Test
 | 
				
			||||||
 | 
						public void constructorWhenIdContainsPrefixThenIllegalArgumentException() {
 | 
				
			||||||
 | 
							this.delegates.put('$' + this.bcryptId, this.bcrypt);
 | 
				
			||||||
 | 
							assertThatIllegalArgumentException()
 | 
				
			||||||
 | 
									.isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "$", "$"));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Test
 | 
				
			||||||
 | 
						public void constructorWhenIdContainsSuffixThenIllegalArgumentException() {
 | 
				
			||||||
 | 
							this.delegates.put(this.bcryptId + '$', this.bcrypt);
 | 
				
			||||||
 | 
							assertThatIllegalArgumentException()
 | 
				
			||||||
 | 
									.isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "", "$"));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Test
 | 
						@Test
 | 
				
			||||||
	public void setDefaultPasswordEncoderForMatchesWhenNullThenIllegalArgumentException() {
 | 
						public void setDefaultPasswordEncoderForMatchesWhenNullThenIllegalArgumentException() {
 | 
				
			||||||
		assertThatIllegalArgumentException()
 | 
							assertThatIllegalArgumentException()
 | 
				
			||||||
| 
						 | 
					@ -104,6 +152,12 @@ public class DelegatingPasswordEncoderTests {
 | 
				
			||||||
		assertThat(this.passwordEncoder.encode(this.rawPassword)).isEqualTo(this.bcryptEncodedPassword);
 | 
							assertThat(this.passwordEncoder.encode(this.rawPassword)).isEqualTo(this.bcryptEncodedPassword);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Test
 | 
				
			||||||
 | 
						public void encodeWhenValidBySpecifyDelegatingPasswordEncoderThenUsesIdForEncode() {
 | 
				
			||||||
 | 
							given(this.bcrypt.encode(this.rawPassword)).willReturn(this.encodedPassword);
 | 
				
			||||||
 | 
							assertThat(this.onlySuffixPasswordEncoder.encode(this.rawPassword)).isEqualTo("bcrypt$" + this.encodedPassword);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Test
 | 
						@Test
 | 
				
			||||||
	public void matchesWhenBCryptThenDelegatesToBCrypt() {
 | 
						public void matchesWhenBCryptThenDelegatesToBCrypt() {
 | 
				
			||||||
		given(this.bcrypt.matches(this.rawPassword, this.encodedPassword)).willReturn(true);
 | 
							given(this.bcrypt.matches(this.rawPassword, this.encodedPassword)).willReturn(true);
 | 
				
			||||||
| 
						 | 
					@ -112,6 +166,14 @@ public class DelegatingPasswordEncoderTests {
 | 
				
			||||||
		verifyZeroInteractions(this.noop);
 | 
							verifyZeroInteractions(this.noop);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Test
 | 
				
			||||||
 | 
						public void matchesWhenBCryptBySpecifyDelegatingPasswordEncoderThenDelegatesToBCrypt() {
 | 
				
			||||||
 | 
							given(this.bcrypt.matches(this.rawPassword, this.encodedPassword)).willReturn(true);
 | 
				
			||||||
 | 
							assertThat(this.onlySuffixPasswordEncoder.matches(this.rawPassword, "bcrypt$" + this.encodedPassword)).isTrue();
 | 
				
			||||||
 | 
							verify(this.bcrypt).matches(this.rawPassword, this.encodedPassword);
 | 
				
			||||||
 | 
							verifyZeroInteractions(this.noop);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Test
 | 
						@Test
 | 
				
			||||||
	public void matchesWhenNoopThenDelegatesToNoop() {
 | 
						public void matchesWhenNoopThenDelegatesToNoop() {
 | 
				
			||||||
		given(this.noop.matches(this.rawPassword, this.encodedPassword)).willReturn(true);
 | 
							given(this.noop.matches(this.rawPassword, this.encodedPassword)).willReturn(true);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue