diff --git a/docs/modules/ROOT/pages/servlet/authentication/rememberme.adoc b/docs/modules/ROOT/pages/servlet/authentication/rememberme.adoc index 5e75fc219e..1d85c90377 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/rememberme.adoc +++ b/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 `` element: +==== [source,xml] ---- @@ -45,6 +49,7 @@ If you are familiar with the topics discussed in the chapter on xref:servlet/con ---- +==== 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] ---- @@ -62,9 +68,11 @@ To use the this approach with namespace configuration, you would supply a dataso ---- +==== 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"] +---- + + + + + + + + + + +---- +==== + +The following beans are required in an application context to enable remember-me services: + +==== [source,xml] ---- ---- +==== 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. 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 2facda2bc1..e624cb5334 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 @@ -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 { *

* 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 +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; + } + + } + } 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 09df5e8a53..7d91141c44 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 @@ -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); + } + }