From e17fe8ced998f2645d3dc159e821b35f68411dcb Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Thu, 30 Jun 2022 14:25:09 -0300 Subject: [PATCH] Add SHA256 as an algorithm option for Remember Me token hashing Closes gh-8549 --- .../servlet/authentication/rememberme.adoc | 52 +++++- .../TokenBasedRememberMeServices.java | 111 ++++++++++-- .../security/test/web/CodecTestUtils.java | 13 ++ .../TokenBasedRememberMeServicesTests.java | 159 ++++++++++++++++-- 4 files changed, 303 insertions(+), 32 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/authentication/rememberme.adoc b/docs/modules/ROOT/pages/servlet/authentication/rememberme.adoc index d6cd44cfe6..a0f92dbe1b 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/rememberme.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/rememberme.adoc @@ -19,13 +19,14 @@ In essence, a cookie is sent to the browser upon successful interactive authenti ==== [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 ---- ==== @@ -113,6 +114,53 @@ A `key` is shared between this authentication provider and the `TokenBasedRememb In addition, `TokenBasedRememberMeServices` requires a `UserDetailsService`, from which it can retrieve the username and password for signature comparison purposes and generate the `RememberMeAuthenticationToken` to contain the correct `GrantedAuthority` instances. `TokenBasedRememberMeServices` also implements Spring Security's `LogoutHandler` interface so that it can be used with `LogoutFilter` to have the cookie cleared automatically. +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 + .authorizeRequests((authorize) -> authorize + .anyRequest().authenticated() + ) + .rememberMe((remember) -> remember + .rememberMeServices(rememberMeServices) + ); + return http.build(); +} + +@Bean +RememberMeServices rememberMeServices(UserDetailsService userDetailsService) { + TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices(myKey, userDetailsService); + rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5); + rememberMe.setEncodingAlgorithm(RememberMeTokenAlgorithm.SHA256); + return rememberMe; +} +---- +.XML +[source,xml,role="secondary"] +---- + + + + + + + + + + +---- +==== + The following beans are required in an application context to enable remember-me services: ==== diff --git a/web/src/main/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServices.java b/web/src/main/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServices.java index ddd7d3cb9b..30d9c3af00 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServices.java +++ b/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: * *
- * username + ":" + expiryTime + ":"
- * 		+ Md5Hex(username + ":" + expiryTime + ":" + password + ":" + key)
+ * username + ":" + expiryTime + ":" + algorithmName + ":"
+ * 		+ algorithmHex(username + ":" + expiryTime + ":" + password + ":" + key)
  * 
* *

+ * 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. + *

+ * + *

* 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 @@ -83,6 +93,14 @@ import org.springframework.util.StringUtils; */ public class TokenBasedRememberMeServices extends AbstractRememberMeServices { + private static final RememberMeTokenAlgorithm DEFAULT_MATCHING_ALGORITHM = RememberMeTokenAlgorithm.MD5; + + private static final RememberMeTokenAlgorithm DEFAULT_ENCODING_ALGORITHM = RememberMeTokenAlgorithm.MD5; + + private RememberMeTokenAlgorithm matchingAlgorithm = DEFAULT_MATCHING_ALGORITHM; + + private RememberMeTokenAlgorithm encodingAlgorithm = DEFAULT_ENCODING_ALGORITHM; + public TokenBasedRememberMeServices(String key, UserDetailsService userDetailsService) { super(key, userDetailsService); } @@ -90,9 +108,9 @@ public class TokenBasedRememberMeServices extends AbstractRememberMeServices { @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 +128,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 +160,32 @@ 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. + */ + 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 +217,33 @@ 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 encode the token signature + * @param encodingAlgorithm the encoding algorithm + */ + public void setEncodingAlgorithm(RememberMeTokenAlgorithm encodingAlgorithm) { + Assert.notNull(encodingAlgorithm, "encodingAlgorithm cannot be null"); + this.encodingAlgorithm = encodingAlgorithm; + } + + /** + * Sets the algorithm to be used to match the token signature + * @param matchingAlgorithm the matching algorithm + */ + 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 +253,7 @@ public class TokenBasedRememberMeServices extends AbstractRememberMeServices { *

* The returned value will be used to work out the expiry time of the token and will * also be used to set the maxAge property of the cookie. - * + *

* See SEC-485. * @param request the request passed to onLoginSuccess * @param authentication the successful authentication object. @@ -234,4 +297,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; + } + + } + } diff --git a/web/src/test/java/org/springframework/security/test/web/CodecTestUtils.java b/web/src/test/java/org/springframework/security/test/web/CodecTestUtils.java index e05ca3b6d8..4a2e9c35bc 100644 --- a/web/src/test/java/org/springframework/security/test/web/CodecTestUtils.java +++ b/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!"); + } + } + } diff --git a/web/src/test/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServicesTests.java b/web/src/test/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServicesTests.java index d90f53ee10..1cd1794ad7 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServicesTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServicesTests.java @@ -32,6 +32,7 @@ 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.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -76,8 +77,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]); } @@ -87,15 +88,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(); @@ -119,8 +157,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(); @@ -160,8 +198,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(); @@ -188,8 +226,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(); @@ -203,8 +241,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(); @@ -215,8 +253,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(); @@ -225,6 +263,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); @@ -292,6 +392,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.setEncodingAlgorithm(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() {