Add SHA256 as an algorithm option for Remember Me token hashing

Closes gh-8549
This commit is contained in:
Marcus Da Coregio 2022-06-30 14:25:09 -03:00
parent 5dff157755
commit f45c4d4b8e
4 changed files with 344 additions and 35 deletions

View File

@ -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. 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: In essence a cookie is sent to the browser upon successful interactive authentication, with the cookie being composed as follows:
====
[source,txt] [source,txt]
---- ----
base64(username + ":" + expirationTime + ":" + base64(username + ":" + expirationTime + ":" + algorithmName + ":"
md5Hex(username + ":" + expirationTime + ":" password + ":" + key)) algorithmHex(username + ":" + expirationTime + ":" password + ":" + key))
username: As identifiable to the UserDetailsService username: As identifiable to the UserDetailsService
password: That matches the one in the retrieved UserDetails password: That matches the one in the retrieved UserDetails
expirationTime: The date and time when the remember-me token expires, expressed in milliseconds 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 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. 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. 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: 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] [source,xml]
---- ----
<http> <http>
@ -45,6 +49,7 @@ If you are familiar with the topics discussed in the chapter on xref:servlet/con
<remember-me key="myAppKey"/> <remember-me key="myAppKey"/>
</http> </http>
---- ----
====
The `UserDetailsService` will normally be selected automatically. 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. 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.]. 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: To use the this approach with namespace configuration, you would supply a datasource reference:
====
[source,xml] [source,xml]
---- ----
<http> <http>
@ -62,9 +68,11 @@ To use the this approach with namespace configuration, you would supply a dataso
<remember-me data-source-ref="someDataSource"/> <remember-me data-source-ref="someDataSource"/>
</http> </http>
---- ----
====
The database should contain a `persistent_logins` table, created using the following SQL (or equivalent): The database should contain a `persistent_logins` table, created using the following SQL (or equivalent):
====
[source,ddl] [source,ddl]
---- ----
create table persistent_logins (username varchar(64) not null, 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, token varchar(64) not null,
last_used timestamp not null) last_used timestamp not null)
---- ----
====
[[remember-me-impls]] [[remember-me-impls]]
== Remember-Me Interfaces and Implementations == 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 hooks will invoke a concrete `RememberMeServices` at the appropriate times.
The interface looks like this: The interface looks like this:
====
[source,java] [source,java]
---- ----
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response); Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
@ -89,6 +99,7 @@ void loginFail(HttpServletRequest request, HttpServletResponse response);
void loginSuccess(HttpServletRequest request, HttpServletResponse response, void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication); 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. 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`. 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. 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. `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] [source,xml]
---- ----
<bean id="rememberMeFilter" class= <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"/> <property name="key" value="springRocks"/>
</bean> </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`). 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 === 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. 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.
There are two standard implementations.
* `InMemoryTokenRepositoryImpl` which is intended for testing only. * `InMemoryTokenRepositoryImpl` which is intended for testing only.
* `JdbcTokenRepositoryImpl` which stores the tokens in a database. * `JdbcTokenRepositoryImpl` which stores the tokens in a database.

View File

@ -54,11 +54,21 @@ import org.springframework.util.StringUtils;
* The cookie encoded by this implementation adopts the following form: * The cookie encoded by this implementation adopts the following form:
* *
* <pre> * <pre>
* username + &quot;:&quot; + expiryTime + &quot;:&quot; * username + &quot;:&quot; + expiryTime + &quot;:&quot; + algorithmName + &quot;:&quot;
* + Md5Hex(username + &quot;:&quot; + expiryTime + &quot;:&quot; + password + &quot;:&quot; + key) * + algorithmHex(username + &quot;:&quot; + expiryTime + &quot;:&quot; + password + &quot;:&quot; + key)
* </pre> * </pre>
* *
* <p> * <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. * 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 * 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 * 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. * not be stored when the browser is closed.
* *
* @author Ben Alex * @author Ben Alex
* @author Marcus Da Coregio
*/ */
public class TokenBasedRememberMeServices extends AbstractRememberMeServices { 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) { 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); super(key, userDetailsService);
Assert.notNull(encodingAlgorithm, "encodingAlgorithm cannot be null");
this.encodingAlgorithm = encodingAlgorithm;
} }
@Override @Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
HttpServletResponse response) { HttpServletResponse response) {
if (cookieTokens.length != 3) { if (!isValidCookieTokensLength(cookieTokens)) {
throw new InvalidCookieException( 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); long tokenExpiryTime = getTokenExpiryTime(cookieTokens);
if (isTokenExpired(tokenExpiryTime)) { 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 // only called once per HttpSession - if the token is valid, it will cause
// SecurityContextHolder population, whilst if invalid, will cause the cookie to // SecurityContextHolder population, whilst if invalid, will cause the cookie to
// be cancelled. // 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(), String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),
userDetails.getPassword()); userDetails.getPassword(), actualAlgorithm);
if (!equals(expectedTokenSignature, cookieTokens[2])) { if (!equals(expectedTokenSignature, actualTokenSignature)) {
throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2] throw new InvalidCookieException("Cookie contained signature '" + actualTokenSignature + "' but expected '"
+ "' but expected '" + expectedTokenSignature + "'"); + expectedTokenSignature + "'");
} }
return userDetails; return userDetails;
} }
private boolean isValidCookieTokensLength(String[] cookieTokens) {
return cookieTokens.length == 3 || cookieTokens.length == 4;
}
private long getTokenExpiryTime(String[] cookieTokens) { private long getTokenExpiryTime(String[] cookieTokens) {
try { try {
return new Long(cookieTokens[1]); 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 * Calculates the digital signature to be put in the cookie. Default value is
* ("username:tokenExpiryTime:password:key") * {@link #encodingAlgorithm} applied to ("username:tokenExpiryTime:password:key")
*/ */
protected String makeTokenSignature(long tokenExpiryTime, String username, String password) { protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey(); String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
try { try {
MessageDigest digest = MessageDigest.getInstance("MD5"); MessageDigest digest = MessageDigest.getInstance(this.encodingAlgorithm.getDigestAlgorithm());
return new String(Hex.encode(digest.digest(data.getBytes()))); return new String(Hex.encode(digest.digest(data.getBytes())));
} }
catch (NoSuchAlgorithmException ex) { 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(); long expiryTime = System.currentTimeMillis();
// SEC-949 // SEC-949
expiryTime += 1000L * ((tokenLifetime < 0) ? TWO_WEEKS_S : tokenLifetime); expiryTime += 1000L * ((tokenLifetime < 0) ? TWO_WEEKS_S : tokenLifetime);
String signatureValue = makeTokenSignature(expiryTime, username, password); String signatureValue = makeTokenSignature(expiryTime, username, password, this.encodingAlgorithm);
setCookie(new String[] { username, Long.toString(expiryTime), signatureValue }, tokenLifetime, request, setCookie(new String[] { username, Long.toString(expiryTime), this.encodingAlgorithm.name(), signatureValue },
response); tokenLifetime, request, response);
if (this.logger.isDebugEnabled()) { if (this.logger.isDebugEnabled()) {
this.logger.debug( this.logger.debug(
"Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'"); "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. * 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 * After this period (from the current time) the remember-me login will be considered
@ -190,7 +262,7 @@ public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
* <p> * <p>
* The returned value will be used to work out the expiry time of the token and will * 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. * also be used to set the <tt>maxAge</tt> property of the cookie.
* * <p>
* See SEC-485. * See SEC-485.
* @param request the request passed to onLoginSuccess * @param request the request passed to onLoginSuccess
* @param authentication the successful authentication object. * @param authentication the successful authentication object.
@ -234,4 +306,20 @@ public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
return (s != null) ? Utf8.encode(s) : null; 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;
}
}
} }

View File

@ -16,8 +16,11 @@
package org.springframework.security.test.web; package org.springframework.security.test.web;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64; import java.util.Base64;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.util.DigestUtils; import org.springframework.util.DigestUtils;
public final class CodecTestUtils { public final class CodecTestUtils {
@ -52,4 +55,14 @@ public final class CodecTestUtils {
return DigestUtils.md5DigestAsHex(data.getBytes()); 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!");
}
}
} }

View File

@ -33,9 +33,12 @@ import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.test.web.CodecTestUtils; 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 org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat; 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.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
@ -47,6 +50,7 @@ import static org.mockito.Mockito.mock;
* . * .
* *
* @author Ben Alex * @author Ben Alex
* @author Marcus Da Coregio
*/ */
public class TokenBasedRememberMeServicesTests { public class TokenBasedRememberMeServicesTests {
@ -77,8 +81,8 @@ public class TokenBasedRememberMeServicesTests {
private long determineExpiryTimeFromBased64EncodedToken(String validToken) { private long determineExpiryTimeFromBased64EncodedToken(String validToken) {
String cookieAsPlainText = CodecTestUtils.decodeBase64(validToken); String cookieAsPlainText = CodecTestUtils.decodeBase64(validToken);
String[] cookieTokens = StringUtils.delimitedListToStringArray(cookieAsPlainText, ":"); String[] cookieTokens = getCookieTokens(cookieAsPlainText);
if (cookieTokens.length == 3) { if (isValidCookieTokensLength(cookieTokens)) {
try { try {
return Long.parseLong(cookieTokens[1]); return Long.parseLong(cookieTokens[1]);
} }
@ -88,15 +92,52 @@ public class TokenBasedRememberMeServicesTests {
return -1; 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: // format is:
// username + ":" + expiryTime + ":" + Md5Hex(username + ":" + expiryTime + ":" + // username + ":" + expiryTime + ":" + Md5Hex(username + ":" + expiryTime + ":" +
// password + ":" + key) // 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; String tokenValue = username + ":" + expiryTime + ":" + signatureValue;
return CodecTestUtils.encodeBase64(tokenValue); 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 @Test
public void autoLoginReturnsNullIfNoCookiePresented() { public void autoLoginReturnsNullIfNoCookiePresented() {
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
@ -120,8 +161,8 @@ public class TokenBasedRememberMeServicesTests {
@Test @Test
public void autoLoginReturnsNullForExpiredCookieAndClearsCookie() { public void autoLoginReturnsNullForExpiredCookieAndClearsCookie() {
Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
generateCorrectCookieContentForToken(System.currentTimeMillis() - 1000000, "someone", "password", generateCorrectCookieContentForTokenNoAlgorithmName(System.currentTimeMillis() - 1000000, "someone",
"key")); "password", "key"));
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(cookie); request.setCookies(cookie);
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
@ -161,8 +202,8 @@ public class TokenBasedRememberMeServicesTests {
public void autoLoginClearsCookieIfSignatureBlocksDoesNotMatchExpectedValue() { public void autoLoginClearsCookieIfSignatureBlocksDoesNotMatchExpectedValue() {
udsWillReturnUser(); udsWillReturnUser();
Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
generateCorrectCookieContentForToken(System.currentTimeMillis() + 1000000, "someone", "password", generateCorrectCookieContentForTokenNoAlgorithmName(System.currentTimeMillis() + 1000000, "someone",
"WRONG_KEY")); "password", "WRONG_KEY"));
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(cookie); request.setCookies(cookie);
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
@ -189,8 +230,8 @@ public class TokenBasedRememberMeServicesTests {
public void autoLoginClearsCookieIfUserNotFound() { public void autoLoginClearsCookieIfUserNotFound() {
udsWillThrowNotFound(); udsWillThrowNotFound();
Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
generateCorrectCookieContentForToken(System.currentTimeMillis() + 1000000, "someone", "password", generateCorrectCookieContentForTokenNoAlgorithmName(System.currentTimeMillis() + 1000000, "someone",
"key")); "password", "key"));
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(cookie); request.setCookies(cookie);
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
@ -204,8 +245,8 @@ public class TokenBasedRememberMeServicesTests {
public void autoLoginClearsCookieIfUserServiceMisconfigured() { public void autoLoginClearsCookieIfUserServiceMisconfigured() {
udsWillReturnNull(); udsWillReturnNull();
Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
generateCorrectCookieContentForToken(System.currentTimeMillis() + 1000000, "someone", "password", generateCorrectCookieContentForTokenNoAlgorithmName(System.currentTimeMillis() + 1000000, "someone",
"key")); "password", "key"));
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(cookie); request.setCookies(cookie);
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
@ -216,8 +257,8 @@ public class TokenBasedRememberMeServicesTests {
public void autoLoginWithValidTokenAndUserSucceeds() { public void autoLoginWithValidTokenAndUserSucceeds() {
udsWillReturnUser(); udsWillReturnUser();
Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
generateCorrectCookieContentForToken(System.currentTimeMillis() + 1000000, "someone", "password", generateCorrectCookieContentForTokenNoAlgorithmName(System.currentTimeMillis() + 1000000, "someone",
"key")); "password", "key"));
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(cookie); request.setCookies(cookie);
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
@ -226,6 +267,68 @@ public class TokenBasedRememberMeServicesTests {
assertThat(result.getPrincipal()).isEqualTo(this.user); 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 @Test
public void testGettersSetters() { public void testGettersSetters() {
assertThat(this.services.getUserDetailsService()).isEqualTo(this.uds); assertThat(this.services.getUserDetailsService()).isEqualTo(this.uds);
@ -293,6 +396,37 @@ public class TokenBasedRememberMeServicesTests {
assertThat(new Date().before(new Date(determineExpiryTimeFromBased64EncodedToken(cookie.getValue())))).isTrue(); 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 // SEC-933
@Test @Test
public void obtainPasswordReturnsNullForTokenWithNullCredentials() { public void obtainPasswordReturnsNullForTokenWithNullCredentials() {
@ -318,4 +452,19 @@ public class TokenBasedRememberMeServicesTests {
assertThat(CodecTestUtils.isBase64(cookie.getValue().getBytes())).isTrue(); 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);
}
} }