Przeglądaj źródła

Add SHA256 as an algorithm option for Remember Me token hashing

Closes gh-8549
Marcus Da Coregio 3 lat temu
rodzic
commit
f45c4d4b8e

+ 64 - 5
docs/modules/ROOT/pages/servlet/authentication/rememberme.adoc

@@ -18,16 +18,19 @@ If you are using an authentication provider which doesn't use a `UserDetailsServ
 This approach uses hashing to achieve a useful remember-me strategy.
 In essence a cookie is sent to the browser upon successful interactive authentication, with the cookie being composed as follows:
 
+====
 [source,txt]
 ----
-base64(username + ":" + expirationTime + ":" +
-md5Hex(username + ":" + expirationTime + ":" password + ":" + key))
+base64(username + ":" + expirationTime + ":" + algorithmName + ":"
+algorithmHex(username + ":" + expirationTime + ":" password + ":" + key))
 
 username:          As identifiable to the UserDetailsService
 password:          That matches the one in the retrieved UserDetails
 expirationTime:    The date and time when the remember-me token expires, expressed in milliseconds
 key:               A private key to prevent modification of the remember-me token
+algorithmName:     The algorithm used to generate and to verify the remember-me token signature
 ----
+====
 
 As such the remember-me token is valid only for the period specified, and provided that the username, password and key does not change.
 Notably, this has a potential security issue in that a captured remember-me token will be usable from any user agent until such time as the token expires.
@@ -38,6 +41,7 @@ Alternatively, remember-me services should simply not be used at all.
 
 If you are familiar with the topics discussed in the chapter on xref:servlet/configuration/xml-namespace.adoc#ns-config[namespace configuration], you can enable remember-me authentication just by adding the `<remember-me>` element:
 
+====
 [source,xml]
 ----
 <http>
@@ -45,6 +49,7 @@ If you are familiar with the topics discussed in the chapter on xref:servlet/con
 <remember-me key="myAppKey"/>
 </http>
 ----
+====
 
 The `UserDetailsService` will normally be selected automatically.
 If you have more than one in your application context, you need to specify which one should be used with the `user-service-ref` attribute, where the value is the name of your `UserDetailsService` bean.
@@ -55,6 +60,7 @@ This approach is based on the article https://web.archive.org/web/20180819014446
 There is a discussion on this in the comments section of this article.].
 To use the this approach with namespace configuration, you would supply a datasource reference:
 
+====
 [source,xml]
 ----
 <http>
@@ -62,9 +68,11 @@ To use the this approach with namespace configuration, you would supply a dataso
 <remember-me data-source-ref="someDataSource"/>
 </http>
 ----
+====
 
 The database should contain a `persistent_logins` table, created using the following SQL (or equivalent):
 
+====
 [source,ddl]
 ----
 create table persistent_logins (username varchar(64) not null,
@@ -72,6 +80,7 @@ create table persistent_logins (username varchar(64) not null,
 								token varchar(64) not null,
 								last_used timestamp not null)
 ----
+====
 
 [[remember-me-impls]]
 == Remember-Me Interfaces and Implementations
@@ -80,6 +89,7 @@ It is also used within `BasicAuthenticationFilter`.
 The hooks will invoke a concrete `RememberMeServices` at the appropriate times.
 The interface looks like this:
 
+====
 [source,java]
 ----
 Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
@@ -89,6 +99,7 @@ void loginFail(HttpServletRequest request, HttpServletResponse response);
 void loginSuccess(HttpServletRequest request, HttpServletResponse response,
 	Authentication successfulAuthentication);
 ----
+====
 
 Please refer to the Javadoc for a fuller discussion on what the methods do, although note at this stage that `AbstractAuthenticationProcessingFilter` only calls the `loginFail()` and `loginSuccess()` methods.
 The `autoLogin()` method is called by `RememberMeAuthenticationFilter` whenever the `SecurityContextHolder` does not contain an `Authentication`.
@@ -105,8 +116,56 @@ In addition, `TokenBasedRememberMeServices` requires A UserDetailsService from w
 Some sort of logout command should be provided by the application that invalidates the cookie if the user requests this.
 `TokenBasedRememberMeServices` also implements Spring Security's `LogoutHandler` interface so can be used with `LogoutFilter` to have the cookie cleared automatically.
 
-The beans required in an application context to enable remember-me services are as follows:
+By default, this implementation uses the MD5 algorithm to encode the token signature.
+To verify the token signature, the algorithm retrieved from `algorithmName` is parsed and used.
+If no `algorithmName` is present, the default matching algorithm will be used, which is MD5.
+You can specify different algorithms for signature encoding and for signature matching, this allows users to safely upgrade to a different encoding algorithm while still able to verify old ones if there is no `algorithmName` present.
+To do that you can specify your customized `TokenBasedRememberMeServices` as a Bean and use it in the configuration.
 
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+SecurityFilterChain securityFilterChain(HttpSecurity http, RememberMeServices rememberMeServices) throws Exception {
+	http
+			.authorizeHttpRequests((authorize) -> authorize
+					.anyRequest().authenticated()
+			)
+			.rememberMe((remember) -> remember
+				.rememberMeServices(rememberMeServices)
+			);
+	return http.build();
+}
+
+@Bean
+RememberMeServices rememberMeServices(UserDetailsService userDetailsService) {
+	RememberMeTokenAlgorithm encodingAlgorithm = RememberMeTokenAlgorithm.SHA256;
+	TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices(myKey, userDetailsService, encodingAlgorithm);
+	rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5);
+	return rememberMe;
+}
+----
+.XML
+[source,xml,role="secondary"]
+----
+<http>
+  <remember-me services-ref="rememberMeServices"/>
+</http>
+
+<bean id="rememberMeServices" class=
+"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
+    <property name="userDetailsService" ref="myUserDetailsService"/>
+    <property name="key" value="springRocks"/>
+    <property name="matchingAlgorithm" value="MD5"/>
+    <property name="encodingAlgorithm" value="SHA256"/>
+</bean>
+----
+====
+
+The following beans are required in an application context to enable remember-me services:
+
+====
 [source,xml]
 ----
 <bean id="rememberMeFilter" class=
@@ -126,13 +185,13 @@ The beans required in an application context to enable remember-me services are
 <property name="key" value="springRocks"/>
 </bean>
 ----
+====
 
 Don't forget to add your `RememberMeServices` implementation to your `UsernamePasswordAuthenticationFilter.setRememberMeServices()` property, include the `RememberMeAuthenticationProvider` in your `AuthenticationManager.setProviders()` list, and add `RememberMeAuthenticationFilter` into your `FilterChainProxy` (typically immediately after your `UsernamePasswordAuthenticationFilter`).
 
 
 === PersistentTokenBasedRememberMeServices
-This class can be used in the same way as `TokenBasedRememberMeServices`, but it additionally needs to be configured with a `PersistentTokenRepository` to store the tokens.
-There are two standard implementations.
+You can use this class in the same way as `TokenBasedRememberMeServices`, but it additionally needs to be configured with a `PersistentTokenRepository` to store the tokens.
 
 * `InMemoryTokenRepositoryImpl` which is intended for testing only.
 * `JdbcTokenRepositoryImpl` which stores the tokens in a database.

+ 104 - 16
web/src/main/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServices.java

@@ -54,11 +54,21 @@ import org.springframework.util.StringUtils;
  * The cookie encoded by this implementation adopts the following form:
  *
  * <pre>
- * username + &quot;:&quot; + expiryTime + &quot;:&quot;
- * 		+ Md5Hex(username + &quot;:&quot; + expiryTime + &quot;:&quot; + password + &quot;:&quot; + key)
+ * username + &quot;:&quot; + expiryTime + &quot;:&quot; + algorithmName + &quot;:&quot;
+ * 		+ algorithmHex(username + &quot;:&quot; + expiryTime + &quot;:&quot; + password + &quot;:&quot; + key)
  * </pre>
  *
  * <p>
+ * This implementation uses the algorithm configured in {@link #encodingAlgorithm} to
+ * encode the signature. It will try to use the algorithm retrieved from the
+ * {@code algorithmName} to validate the signature. However, if the {@code algorithmName}
+ * is not present in the cookie value, the algorithm configured in
+ * {@link #matchingAlgorithm} will be used to validate the signature. This allows users to
+ * safely upgrade to a different encoding algorithm while still able to verify old ones if
+ * there is no {@code algorithmName} present.
+ * </p>
+ *
+ * <p>
  * As such, if the user changes their password, any remember-me token will be invalidated.
  * Equally, the system administrator may invalidate every remember-me token on issue by
  * changing the key. This provides some reasonable approaches to recovering from a
@@ -80,19 +90,43 @@ import org.springframework.util.StringUtils;
  * not be stored when the browser is closed.
  *
  * @author Ben Alex
+ * @author Marcus Da Coregio
  */
 public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
 
+	private static final RememberMeTokenAlgorithm DEFAULT_MATCHING_ALGORITHM = RememberMeTokenAlgorithm.MD5;
+
+	private static final RememberMeTokenAlgorithm DEFAULT_ENCODING_ALGORITHM = RememberMeTokenAlgorithm.MD5;
+
+	private final RememberMeTokenAlgorithm encodingAlgorithm;
+
+	private RememberMeTokenAlgorithm matchingAlgorithm = DEFAULT_MATCHING_ALGORITHM;
+
 	public TokenBasedRememberMeServices(String key, UserDetailsService userDetailsService) {
+		this(key, userDetailsService, DEFAULT_ENCODING_ALGORITHM);
+	}
+
+	/**
+	 * Construct the instance with the parameters provided
+	 * @param key the signature key
+	 * @param userDetailsService the {@link UserDetailsService}
+	 * @param encodingAlgorithm the {@link RememberMeTokenAlgorithm} used to encode the
+	 * signature
+	 * @since 5.8
+	 */
+	public TokenBasedRememberMeServices(String key, UserDetailsService userDetailsService,
+			RememberMeTokenAlgorithm encodingAlgorithm) {
 		super(key, userDetailsService);
+		Assert.notNull(encodingAlgorithm, "encodingAlgorithm cannot be null");
+		this.encodingAlgorithm = encodingAlgorithm;
 	}
 
 	@Override
 	protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
 			HttpServletResponse response) {
-		if (cookieTokens.length != 3) {
+		if (!isValidCookieTokensLength(cookieTokens)) {
 			throw new InvalidCookieException(
-					"Cookie token did not contain 3" + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
+					"Cookie token did not contain 3 or 4 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
 		}
 		long tokenExpiryTime = getTokenExpiryTime(cookieTokens);
 		if (isTokenExpired(tokenExpiryTime)) {
@@ -110,15 +144,27 @@ public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
 		// only called once per HttpSession - if the token is valid, it will cause
 		// SecurityContextHolder population, whilst if invalid, will cause the cookie to
 		// be cancelled.
+		String actualTokenSignature = cookieTokens[2];
+		RememberMeTokenAlgorithm actualAlgorithm = this.matchingAlgorithm;
+		// If the cookie value contains the algorithm, we use that algorithm to check the
+		// signature
+		if (cookieTokens.length == 4) {
+			actualTokenSignature = cookieTokens[3];
+			actualAlgorithm = RememberMeTokenAlgorithm.valueOf(cookieTokens[2]);
+		}
 		String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),
-				userDetails.getPassword());
-		if (!equals(expectedTokenSignature, cookieTokens[2])) {
-			throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2]
-					+ "' but expected '" + expectedTokenSignature + "'");
+				userDetails.getPassword(), actualAlgorithm);
+		if (!equals(expectedTokenSignature, actualTokenSignature)) {
+			throw new InvalidCookieException("Cookie contained signature '" + actualTokenSignature + "' but expected '"
+					+ expectedTokenSignature + "'");
 		}
 		return userDetails;
 	}
 
+	private boolean isValidCookieTokensLength(String[] cookieTokens) {
+		return cookieTokens.length == 3 || cookieTokens.length == 4;
+	}
+
 	private long getTokenExpiryTime(String[] cookieTokens) {
 		try {
 			return new Long(cookieTokens[1]);
@@ -130,17 +176,33 @@ public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
 	}
 
 	/**
-	 * Calculates the digital signature to be put in the cookie. Default value is MD5
-	 * ("username:tokenExpiryTime:password:key")
+	 * Calculates the digital signature to be put in the cookie. Default value is
+	 * {@link #encodingAlgorithm} applied to ("username:tokenExpiryTime:password:key")
 	 */
 	protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
 		String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
 		try {
-			MessageDigest digest = MessageDigest.getInstance("MD5");
+			MessageDigest digest = MessageDigest.getInstance(this.encodingAlgorithm.getDigestAlgorithm());
 			return new String(Hex.encode(digest.digest(data.getBytes())));
 		}
 		catch (NoSuchAlgorithmException ex) {
-			throw new IllegalStateException("No MD5 algorithm available!");
+			throw new IllegalStateException("No " + this.encodingAlgorithm.name() + " algorithm available!");
+		}
+	}
+
+	/**
+	 * Calculates the digital signature to be put in the cookie.
+	 * @since 5.8
+	 */
+	protected String makeTokenSignature(long tokenExpiryTime, String username, String password,
+			RememberMeTokenAlgorithm algorithm) {
+		String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
+		try {
+			MessageDigest digest = MessageDigest.getInstance(algorithm.getDigestAlgorithm());
+			return new String(Hex.encode(digest.digest(data.getBytes())));
+		}
+		catch (NoSuchAlgorithmException ex) {
+			throw new IllegalStateException("No " + algorithm.name() + " algorithm available!");
 		}
 	}
 
@@ -172,15 +234,25 @@ public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
 		long expiryTime = System.currentTimeMillis();
 		// SEC-949
 		expiryTime += 1000L * ((tokenLifetime < 0) ? TWO_WEEKS_S : tokenLifetime);
-		String signatureValue = makeTokenSignature(expiryTime, username, password);
-		setCookie(new String[] { username, Long.toString(expiryTime), signatureValue }, tokenLifetime, request,
-				response);
+		String signatureValue = makeTokenSignature(expiryTime, username, password, this.encodingAlgorithm);
+		setCookie(new String[] { username, Long.toString(expiryTime), this.encodingAlgorithm.name(), signatureValue },
+				tokenLifetime, request, response);
 		if (this.logger.isDebugEnabled()) {
 			this.logger.debug(
 					"Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'");
 		}
 	}
 
+	/**
+	 * Sets the algorithm to be used to match the token signature
+	 * @param matchingAlgorithm the matching algorithm
+	 * @since 5.8
+	 */
+	public void setMatchingAlgorithm(RememberMeTokenAlgorithm matchingAlgorithm) {
+		Assert.notNull(matchingAlgorithm, "matchingAlgorithm cannot be null");
+		this.matchingAlgorithm = matchingAlgorithm;
+	}
+
 	/**
 	 * Calculates the validity period in seconds for a newly generated remember-me login.
 	 * After this period (from the current time) the remember-me login will be considered
@@ -190,7 +262,7 @@ public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
 	 * <p>
 	 * The returned value will be used to work out the expiry time of the token and will
 	 * also be used to set the <tt>maxAge</tt> property of the cookie.
-	 *
+	 * <p>
 	 * See SEC-485.
 	 * @param request the request passed to onLoginSuccess
 	 * @param authentication the successful authentication object.
@@ -234,4 +306,20 @@ public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
 		return (s != null) ? Utf8.encode(s) : null;
 	}
 
+	public enum RememberMeTokenAlgorithm {
+
+		MD5("MD5"), SHA256("SHA-256");
+
+		private final String digestAlgorithm;
+
+		RememberMeTokenAlgorithm(String digestAlgorithm) {
+			this.digestAlgorithm = digestAlgorithm;
+		}
+
+		public String getDigestAlgorithm() {
+			return this.digestAlgorithm;
+		}
+
+	}
+
 }

+ 13 - 0
web/src/test/java/org/springframework/security/test/web/CodecTestUtils.java

@@ -16,8 +16,11 @@
 
 package org.springframework.security.test.web;
 
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.Base64;
 
+import org.springframework.security.crypto.codec.Hex;
 import org.springframework.util.DigestUtils;
 
 public final class CodecTestUtils {
@@ -52,4 +55,14 @@ public final class CodecTestUtils {
 		return DigestUtils.md5DigestAsHex(data.getBytes());
 	}
 
+	public static String algorithmHex(String algorithmName, String data) {
+		try {
+			MessageDigest digest = MessageDigest.getInstance(algorithmName);
+			return new String(Hex.encode(digest.digest(data.getBytes())));
+		}
+		catch (NoSuchAlgorithmException ex) {
+			throw new IllegalStateException("No " + algorithmName + " algorithm available!");
+		}
+	}
+
 }

+ 163 - 14
web/src/test/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServicesTests.java

@@ -33,9 +33,12 @@ import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.security.test.web.CodecTestUtils;
+import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.RememberMeTokenAlgorithm;
+import org.springframework.test.util.ReflectionTestUtils;
 import org.springframework.util.StringUtils;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.BDDMockito.given;
@@ -47,6 +50,7 @@ import static org.mockito.Mockito.mock;
  * .
  *
  * @author Ben Alex
+ * @author Marcus Da Coregio
  */
 public class TokenBasedRememberMeServicesTests {
 
@@ -77,8 +81,8 @@ public class TokenBasedRememberMeServicesTests {
 
 	private long determineExpiryTimeFromBased64EncodedToken(String validToken) {
 		String cookieAsPlainText = CodecTestUtils.decodeBase64(validToken);
-		String[] cookieTokens = StringUtils.delimitedListToStringArray(cookieAsPlainText, ":");
-		if (cookieTokens.length == 3) {
+		String[] cookieTokens = getCookieTokens(cookieAsPlainText);
+		if (isValidCookieTokensLength(cookieTokens)) {
 			try {
 				return Long.parseLong(cookieTokens[1]);
 			}
@@ -88,15 +92,52 @@ public class TokenBasedRememberMeServicesTests {
 		return -1;
 	}
 
-	private String generateCorrectCookieContentForToken(long expiryTime, String username, String password, String key) {
+	private String[] getCookieTokens(String cookieAsPlainText) {
+		return StringUtils.delimitedListToStringArray(cookieAsPlainText, ":");
+	}
+
+	private String determineAlgorithmNameFromBase64EncodedToken(String validToken) {
+		String cookieAsPlainText = CodecTestUtils.decodeBase64(validToken);
+		String[] cookieTokens = getCookieTokens(cookieAsPlainText);
+		if (isValidCookieTokensLength(cookieTokens)) {
+			return cookieTokens[2];
+		}
+		return null;
+	}
+
+	private boolean isValidCookieTokensLength(String[] cookieTokens) {
+		return cookieTokens.length == 3 || cookieTokens.length == 4;
+	}
+
+	private String generateCorrectCookieContentForTokenNoAlgorithmName(long expiryTime, String username,
+			String password, String key) {
+		return generateCorrectCookieContentForTokenWithAlgorithmName(expiryTime, username, password, key,
+				RememberMeTokenAlgorithm.MD5);
+	}
+
+	private String generateCorrectCookieContentForTokenNoAlgorithmName(long expiryTime, String username,
+			String password, String key, RememberMeTokenAlgorithm algorithm) {
 		// format is:
 		// username + ":" + expiryTime + ":" + Md5Hex(username + ":" + expiryTime + ":" +
 		// password + ":" + key)
-		String signatureValue = CodecTestUtils.md5Hex(username + ":" + expiryTime + ":" + password + ":" + key);
+		String signatureValue = CodecTestUtils.algorithmHex(algorithm.getDigestAlgorithm(),
+				username + ":" + expiryTime + ":" + password + ":" + key);
 		String tokenValue = username + ":" + expiryTime + ":" + signatureValue;
 		return CodecTestUtils.encodeBase64(tokenValue);
 	}
 
+	private String generateCorrectCookieContentForTokenWithAlgorithmName(long expiryTime, String username,
+			String password, String key, RememberMeTokenAlgorithm algorithm) {
+		// format is:
+		// username + ":" + expiryTime + ":" + algorithmName + ":" + algorithmHex(username
+		// + ":" + expiryTime + ":" +
+		// password + ":" + key)
+		String signatureValue = CodecTestUtils.algorithmHex(algorithm.getDigestAlgorithm(),
+				username + ":" + expiryTime + ":" + password + ":" + key);
+		String tokenValue = username + ":" + expiryTime + ":" + algorithm.name() + ":" + signatureValue;
+		return CodecTestUtils.encodeBase64(tokenValue);
+	}
+
 	@Test
 	public void autoLoginReturnsNullIfNoCookiePresented() {
 		MockHttpServletResponse response = new MockHttpServletResponse();
@@ -120,8 +161,8 @@ public class TokenBasedRememberMeServicesTests {
 	@Test
 	public void autoLoginReturnsNullForExpiredCookieAndClearsCookie() {
 		Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
-				generateCorrectCookieContentForToken(System.currentTimeMillis() - 1000000, "someone", "password",
-						"key"));
+				generateCorrectCookieContentForTokenNoAlgorithmName(System.currentTimeMillis() - 1000000, "someone",
+						"password", "key"));
 		MockHttpServletRequest request = new MockHttpServletRequest();
 		request.setCookies(cookie);
 		MockHttpServletResponse response = new MockHttpServletResponse();
@@ -161,8 +202,8 @@ public class TokenBasedRememberMeServicesTests {
 	public void autoLoginClearsCookieIfSignatureBlocksDoesNotMatchExpectedValue() {
 		udsWillReturnUser();
 		Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
-				generateCorrectCookieContentForToken(System.currentTimeMillis() + 1000000, "someone", "password",
-						"WRONG_KEY"));
+				generateCorrectCookieContentForTokenNoAlgorithmName(System.currentTimeMillis() + 1000000, "someone",
+						"password", "WRONG_KEY"));
 		MockHttpServletRequest request = new MockHttpServletRequest();
 		request.setCookies(cookie);
 		MockHttpServletResponse response = new MockHttpServletResponse();
@@ -189,8 +230,8 @@ public class TokenBasedRememberMeServicesTests {
 	public void autoLoginClearsCookieIfUserNotFound() {
 		udsWillThrowNotFound();
 		Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
-				generateCorrectCookieContentForToken(System.currentTimeMillis() + 1000000, "someone", "password",
-						"key"));
+				generateCorrectCookieContentForTokenNoAlgorithmName(System.currentTimeMillis() + 1000000, "someone",
+						"password", "key"));
 		MockHttpServletRequest request = new MockHttpServletRequest();
 		request.setCookies(cookie);
 		MockHttpServletResponse response = new MockHttpServletResponse();
@@ -204,8 +245,8 @@ public class TokenBasedRememberMeServicesTests {
 	public void autoLoginClearsCookieIfUserServiceMisconfigured() {
 		udsWillReturnNull();
 		Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
-				generateCorrectCookieContentForToken(System.currentTimeMillis() + 1000000, "someone", "password",
-						"key"));
+				generateCorrectCookieContentForTokenNoAlgorithmName(System.currentTimeMillis() + 1000000, "someone",
+						"password", "key"));
 		MockHttpServletRequest request = new MockHttpServletRequest();
 		request.setCookies(cookie);
 		MockHttpServletResponse response = new MockHttpServletResponse();
@@ -216,8 +257,8 @@ public class TokenBasedRememberMeServicesTests {
 	public void autoLoginWithValidTokenAndUserSucceeds() {
 		udsWillReturnUser();
 		Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
-				generateCorrectCookieContentForToken(System.currentTimeMillis() + 1000000, "someone", "password",
-						"key"));
+				generateCorrectCookieContentForTokenNoAlgorithmName(System.currentTimeMillis() + 1000000, "someone",
+						"password", "key"));
 		MockHttpServletRequest request = new MockHttpServletRequest();
 		request.setCookies(cookie);
 		MockHttpServletResponse response = new MockHttpServletResponse();
@@ -226,6 +267,68 @@ public class TokenBasedRememberMeServicesTests {
 		assertThat(result.getPrincipal()).isEqualTo(this.user);
 	}
 
+	@Test
+	public void autoLoginWhenTokenNoAlgorithmAndDifferentMatchingAlgorithmThenReturnsNullAndClearCookie() {
+		udsWillReturnUser();
+		Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
+				generateCorrectCookieContentForTokenNoAlgorithmName(System.currentTimeMillis() + 1000000, "someone",
+						"password", "key", RememberMeTokenAlgorithm.MD5));
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setCookies(cookie);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		this.services.setMatchingAlgorithm(RememberMeTokenAlgorithm.SHA256);
+		Authentication result = this.services.autoLogin(request, response);
+		Cookie returnedCookie = response.getCookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY);
+		assertThat(result).isNull();
+		assertThat(returnedCookie).isNotNull();
+		assertThat(returnedCookie.getMaxAge()).isZero();
+	}
+
+	@Test
+	public void autoLoginWhenTokenNoAlgorithmAndSameMatchingAlgorithmThenSucceeds() {
+		udsWillReturnUser();
+		Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
+				generateCorrectCookieContentForTokenNoAlgorithmName(System.currentTimeMillis() + 1000000, "someone",
+						"password", "key", RememberMeTokenAlgorithm.SHA256));
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setCookies(cookie);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		this.services.setMatchingAlgorithm(RememberMeTokenAlgorithm.SHA256);
+		Authentication result = this.services.autoLogin(request, response);
+		assertThat(result).isNotNull();
+		assertThat(result.getPrincipal()).isEqualTo(this.user);
+	}
+
+	@Test
+	public void autoLoginWhenTokenHasAlgorithmAndSameMatchingAlgorithmThenUsesTokenAlgorithmAndSucceeds() {
+		udsWillReturnUser();
+		Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
+				generateCorrectCookieContentForTokenWithAlgorithmName(System.currentTimeMillis() + 1000000, "someone",
+						"password", "key", RememberMeTokenAlgorithm.SHA256));
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setCookies(cookie);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		this.services.setMatchingAlgorithm(RememberMeTokenAlgorithm.SHA256);
+		Authentication result = this.services.autoLogin(request, response);
+		assertThat(result).isNotNull();
+		assertThat(result.getPrincipal()).isEqualTo(this.user);
+	}
+
+	@Test
+	public void autoLoginWhenTokenHasAlgorithmAndDifferentMatchingAlgorithmThenUsesTokenAlgorithmAndSucceeds() {
+		udsWillReturnUser();
+		Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
+				generateCorrectCookieContentForTokenWithAlgorithmName(System.currentTimeMillis() + 1000000, "someone",
+						"password", "key", RememberMeTokenAlgorithm.SHA256));
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setCookies(cookie);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		this.services.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5);
+		Authentication result = this.services.autoLogin(request, response);
+		assertThat(result).isNotNull();
+		assertThat(result.getPrincipal()).isEqualTo(this.user);
+	}
+
 	@Test
 	public void testGettersSetters() {
 		assertThat(this.services.getUserDetailsService()).isEqualTo(this.uds);
@@ -293,6 +396,37 @@ public class TokenBasedRememberMeServicesTests {
 		assertThat(new Date().before(new Date(determineExpiryTimeFromBased64EncodedToken(cookie.getValue())))).isTrue();
 	}
 
+	@Test
+	public void loginSuccessWhenDefaultEncodingAlgorithmThenContainsAlgorithmName() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.addParameter(AbstractRememberMeServices.DEFAULT_PARAMETER, "true");
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		this.services.loginSuccess(request, response,
+				new TestingAuthenticationToken("someone", "password", "ROLE_ABC"));
+		Cookie cookie = response.getCookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY);
+		assertThat(cookie).isNotNull();
+		assertThat(cookie.getMaxAge()).isEqualTo(this.services.getTokenValiditySeconds());
+		assertThat(CodecTestUtils.isBase64(cookie.getValue().getBytes())).isTrue();
+		assertThat(new Date().before(new Date(determineExpiryTimeFromBased64EncodedToken(cookie.getValue())))).isTrue();
+		assertThat("MD5").isEqualTo(determineAlgorithmNameFromBase64EncodedToken(cookie.getValue()));
+	}
+
+	@Test
+	public void loginSuccessWhenCustomEncodingAlgorithmThenContainsAlgorithmName() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.addParameter(AbstractRememberMeServices.DEFAULT_PARAMETER, "true");
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		this.services = new TokenBasedRememberMeServices("key", this.uds, RememberMeTokenAlgorithm.SHA256);
+		this.services.loginSuccess(request, response,
+				new TestingAuthenticationToken("someone", "password", "ROLE_ABC"));
+		Cookie cookie = response.getCookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY);
+		assertThat(cookie).isNotNull();
+		assertThat(cookie.getMaxAge()).isEqualTo(this.services.getTokenValiditySeconds());
+		assertThat(CodecTestUtils.isBase64(cookie.getValue().getBytes())).isTrue();
+		assertThat(new Date().before(new Date(determineExpiryTimeFromBased64EncodedToken(cookie.getValue())))).isTrue();
+		assertThat("SHA256").isEqualTo(determineAlgorithmNameFromBase64EncodedToken(cookie.getValue()));
+	}
+
 	// SEC-933
 	@Test
 	public void obtainPasswordReturnsNullForTokenWithNullCredentials() {
@@ -318,4 +452,19 @@ public class TokenBasedRememberMeServicesTests {
 		assertThat(CodecTestUtils.isBase64(cookie.getValue().getBytes())).isTrue();
 	}
 
+	@Test
+	public void constructorWhenEncodingAlgorithmNullThenException() {
+		assertThatExceptionOfType(IllegalArgumentException.class)
+				.isThrownBy(() -> new TokenBasedRememberMeServices("key", this.uds, null))
+				.withMessage("encodingAlgorithm cannot be null");
+	}
+
+	@Test
+	public void constructorWhenNoEncodingAlgorithmSpecifiedThenMd5() {
+		TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("key", this.uds);
+		RememberMeTokenAlgorithm encodingAlgorithm = (RememberMeTokenAlgorithm) ReflectionTestUtils
+				.getField(rememberMeServices, "encodingAlgorithm");
+		assertThat(encodingAlgorithm).isSameAs(RememberMeTokenAlgorithm.MD5);
+	}
+
 }