SEC-588: PersistentTokenBasedRememberMeServices implementation.
This commit is contained in:
parent
8b199d38ed
commit
55b1f9348d
|
@ -0,0 +1,306 @@
|
|||
package org.springframework.security.ui.rememberme;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.security.Authentication;
|
||||
import org.springframework.security.SpringSecurityMessageSource;
|
||||
import org.springframework.security.providers.rememberme.RememberMeAuthenticationToken;
|
||||
import org.springframework.security.ui.AuthenticationDetailsSource;
|
||||
import org.springframework.security.ui.AuthenticationDetailsSourceImpl;
|
||||
import org.springframework.security.userdetails.UserDetails;
|
||||
import org.springframework.security.userdetails.UserDetailsService;
|
||||
import org.springframework.security.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.ServletRequestUtils;
|
||||
import org.springframework.context.support.MessageSourceAccessor;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Base class for RememberMeServices implementations.
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
*/
|
||||
public abstract class AbstractRememberMeServices implements RememberMeServices {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
|
||||
|
||||
public static final String DEFAULT_PARAMETER = "_spring_security_remember_me";
|
||||
public static final String SPRING_SECURITY_PERSISTENT_REMEMBER_ME_COOKIE_KEY = "SPRING_SECURITY_REMEMBER_ME_COOKIE";
|
||||
private static final String DELIMITER = ":";
|
||||
|
||||
private UserDetailsService userDetailsService;
|
||||
private AuthenticationDetailsSource authenticationDetailsSource = new AuthenticationDetailsSourceImpl();
|
||||
|
||||
private String cookieName = SPRING_SECURITY_PERSISTENT_REMEMBER_ME_COOKIE_KEY;
|
||||
private String parameter = DEFAULT_PARAMETER;
|
||||
private boolean alwaysRemember;
|
||||
private String key;
|
||||
private long tokenValiditySeconds = 1209600; // 14 days
|
||||
|
||||
/**
|
||||
* Template implementation which locates the Spring Security cookie, decodes it into
|
||||
* a delimited array of tokens and submits it to subclasses for processing
|
||||
* via the <tt>processAutoLoginCookie</tt> method.
|
||||
* <p>
|
||||
* The returned username is then used to load the UserDetails object for the user, which in turn
|
||||
* is used to create a valid authentication token.
|
||||
*/
|
||||
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
|
||||
String rememberMeCookie = extractRememberMeCookie(request);
|
||||
|
||||
if (rememberMeCookie == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug("Remember-me cookie detected");
|
||||
|
||||
UserDetails user = null;
|
||||
|
||||
try {
|
||||
String[] cookieTokens = decodeCookie(rememberMeCookie);
|
||||
String username = processAutoLoginCookie(cookieTokens, request, response);
|
||||
user = loadAndValidateUserDetails(username);
|
||||
} catch (CookieTheftException cte) {
|
||||
cancelCookie(request, response);
|
||||
throw cte;
|
||||
} catch (UsernameNotFoundException noUser) {
|
||||
cancelCookie(request, response);
|
||||
logger.debug("Remember-me login was valid but corresponding user not found.", noUser);
|
||||
return null;
|
||||
} catch (InvalidCookieException invalidCookie) {
|
||||
cancelCookie(request, response);
|
||||
logger.debug("Invalid remember-me cookie: " + invalidCookie.getMessage());
|
||||
return null;
|
||||
} catch (RememberMeAuthenticationException e) {
|
||||
cancelCookie(request, response);
|
||||
logger.debug("autoLogin failed", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug("Remember-me cookie accepted");
|
||||
|
||||
RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(key, user, user.getAuthorities());
|
||||
auth.setDetails(authenticationDetailsSource.buildDetails(request));
|
||||
|
||||
return auth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the Spring Security remember me cookie in the request.
|
||||
*
|
||||
* @param request the submitted request which is to be authenticated
|
||||
* @return the cookie value (if present), null otherwise.
|
||||
*/
|
||||
private String extractRememberMeCookie(HttpServletRequest request) {
|
||||
Cookie[] cookies = request.getCookies();
|
||||
|
||||
if ((cookies == null) || (cookies.length == 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < cookies.length; i++) {
|
||||
if (cookieName.equals(cookies[i].getName())) {
|
||||
return cookies[i].getValue();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the cookie and splits it into a set of token strings using the ":" delimiter.
|
||||
*
|
||||
* @param cookieValue the value obtained from the submitted cookie
|
||||
* @return the array of tokens.
|
||||
* @throws InvalidCookieException if the cookie was not base64 encoded.
|
||||
*/
|
||||
protected String[] decodeCookie(String cookieValue) throws InvalidCookieException {
|
||||
for (int j = 0; j < cookieValue.length() % 4; j++) {
|
||||
cookieValue = cookieValue + "=";
|
||||
}
|
||||
|
||||
if (!Base64.isArrayByteBase64(cookieValue.getBytes())) {
|
||||
throw new InvalidCookieException( "Cookie token was not Base64 encoded; value was '" + cookieValue + "'");
|
||||
}
|
||||
|
||||
String cookieAsPlainText = new String(Base64.decodeBase64(cookieValue.getBytes()));
|
||||
|
||||
return StringUtils.delimitedListToStringArray(cookieAsPlainText, DELIMITER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverse operation of decodeCookie.
|
||||
*
|
||||
* @param cookieTokens the tokens to be encoded.
|
||||
* @return base64 encoding of the tokens concatenated with the ":" delimiter.
|
||||
*/
|
||||
protected String encodeCookie(String[] cookieTokens) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
for(int i=0; i < cookieTokens.length; i++) {
|
||||
sb.append(cookieTokens[i]);
|
||||
|
||||
if (i < cookieTokens.length - 1) {
|
||||
sb.append(DELIMITER);
|
||||
}
|
||||
}
|
||||
|
||||
String value = sb.toString();
|
||||
|
||||
sb = new StringBuffer(new String(Base64.encodeBase64(value.getBytes())));
|
||||
|
||||
while (sb.charAt(sb.length() - 1) == '=') {
|
||||
sb.deleteCharAt(sb.length() - 1);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
protected UserDetails loadAndValidateUserDetails(String username) throws UsernameNotFoundException,
|
||||
RememberMeAuthenticationException {
|
||||
|
||||
UserDetails user;
|
||||
|
||||
user = this.userDetailsService.loadUserByUsername(username);
|
||||
|
||||
if (!user.isAccountNonExpired() || !user.isCredentialsNonExpired() || !user.isEnabled()) {
|
||||
throw new RememberMeAuthenticationException("Remember-me login was valid for user " +
|
||||
user.getUsername() + ", but account is expired, has expired credentials or is disabled");
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public final void loginFail(HttpServletRequest request, HttpServletResponse response) {
|
||||
cancelCookie(request, response);
|
||||
onLoginFail(request, response);
|
||||
}
|
||||
|
||||
protected void onLoginFail(HttpServletRequest request, HttpServletResponse response) {}
|
||||
|
||||
/**
|
||||
* Examines the incoming request and checks for the presence of the configured "remember me" parameter.
|
||||
* If it's present, or if <tt>alwaysRemember</tt> is set to true, calls <tt>onLoginSucces</tt>.
|
||||
*/
|
||||
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication successfulAuthentication) {
|
||||
|
||||
if (!rememberMeRequested(request, parameter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
onLoginSuccess(request, response, successfulAuthentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from loginSuccess when a remember-me login has been requested.
|
||||
* Typically implemented by subclasses to set a remember-me cookie and potentially store a record
|
||||
* of it if the implementation requires this.
|
||||
*/
|
||||
protected abstract void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication successfulAuthentication);
|
||||
|
||||
/**
|
||||
* Allows customization of whether a remember-me login has been requested.
|
||||
* The default is to return true if <tt>alwaysRemember</tt> is set or the configured parameter name has
|
||||
* been included in the request and is set to the value "true".
|
||||
*
|
||||
* @param request the request which may include
|
||||
* @param parameter the configured remember-me parameter name.
|
||||
*
|
||||
* @return true if the request includes information indicating that a persistent login has been
|
||||
* requested.
|
||||
*/
|
||||
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
|
||||
if (alwaysRemember) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ServletRequestUtils.getBooleanParameter(request, parameter, false)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Did not send remember-me cookie (principal did not set parameter '" + parameter + "')");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from autoLogin to process the submitted pesistent login cookie. Subclasses should
|
||||
* validate the cookie and perform any additional management required.
|
||||
*
|
||||
* @param cookieTokens the decoded and tokenized cookie value
|
||||
* @param request the request
|
||||
* @param response the response, to allow the cookie to be modified if required.
|
||||
* @return the name of the corresponding user account if the cookie was validated successfully.
|
||||
* @throws RememberMeAuthenticationException if the cookie is invalid or the login is invalid for some
|
||||
* other reason.
|
||||
*/
|
||||
protected abstract String processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
|
||||
HttpServletResponse response) throws RememberMeAuthenticationException;
|
||||
|
||||
protected void cancelCookie(HttpServletRequest request, HttpServletResponse response) {
|
||||
logger.debug("Cancelling cookie");
|
||||
|
||||
response.addCookie(makeCancelCookie(request));
|
||||
}
|
||||
|
||||
protected Cookie makeCancelCookie(HttpServletRequest request) {
|
||||
Cookie cookie = new Cookie(cookieName, null);
|
||||
cookie.setMaxAge(0);
|
||||
cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
|
||||
|
||||
return cookie;
|
||||
}
|
||||
|
||||
protected Cookie makeValidCookie(String value, HttpServletRequest request, long maxAge) {
|
||||
Cookie cookie = new Cookie(cookieName, value);
|
||||
cookie.setMaxAge(new Long(maxAge).intValue());
|
||||
cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
|
||||
|
||||
return cookie;
|
||||
}
|
||||
|
||||
public void setCookieName(String cookieName) {
|
||||
this.cookieName = cookieName;
|
||||
}
|
||||
|
||||
public void setAlwaysRemember(boolean alwaysRemember) {
|
||||
this.alwaysRemember = alwaysRemember;
|
||||
}
|
||||
|
||||
public void setParameter(String parameter) {
|
||||
this.parameter = parameter;
|
||||
}
|
||||
|
||||
protected UserDetailsService getUserDetailsService() {
|
||||
return userDetailsService;
|
||||
}
|
||||
|
||||
public void setUserDetailsService(UserDetailsService userDetailsService) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public void setTokenValiditySeconds(long tokenValiditySeconds) {
|
||||
this.tokenValiditySeconds = tokenValiditySeconds;
|
||||
}
|
||||
|
||||
public long getTokenValiditySeconds() {
|
||||
return tokenValiditySeconds;
|
||||
}
|
||||
|
||||
public AuthenticationDetailsSource getAuthenticationDetailsSource() {
|
||||
return authenticationDetailsSource;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package org.springframework.security.ui.rememberme;
|
||||
|
||||
/**
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
*/
|
||||
public class CookieTheftException extends RememberMeAuthenticationException {
|
||||
public CookieTheftException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package org.springframework.security.ui.rememberme;
|
||||
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
*/
|
||||
public class InMemoryTokenRepositoryImpl implements PersistentTokenRepository {
|
||||
private Map seriesTokens = new HashMap();
|
||||
|
||||
public synchronized void saveToken(PersistentRememberMeToken token) {
|
||||
PersistentRememberMeToken current = (PersistentRememberMeToken) seriesTokens.get(token.getSeries());
|
||||
|
||||
if (current != null && !token.getUsername().equals(current.getUsername())) {
|
||||
throw new DataIntegrityViolationException("Series Id already exists with different username");
|
||||
}
|
||||
|
||||
// Store it, overwriting the existing one.
|
||||
seriesTokens.put(token.getSeries(), token);
|
||||
}
|
||||
|
||||
public synchronized PersistentRememberMeToken getTokenForSeries(String seriesId) {
|
||||
return (PersistentRememberMeToken) seriesTokens.get(seriesId);
|
||||
}
|
||||
|
||||
public synchronized void removeAllTokens(String username) {
|
||||
Iterator series = seriesTokens.keySet().iterator();
|
||||
|
||||
while (series.hasNext()) {
|
||||
Object seriesId = series.next();
|
||||
|
||||
PersistentRememberMeToken token = (PersistentRememberMeToken) seriesTokens.get(seriesId);
|
||||
|
||||
if (username.equals(token.getUsername())) {
|
||||
series.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package org.springframework.security.ui.rememberme;
|
||||
|
||||
/**
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
*/
|
||||
public class InvalidCookieException extends RememberMeAuthenticationException {
|
||||
public InvalidCookieException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package org.springframework.security.ui.rememberme;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
*/
|
||||
public class PersistentRememberMeToken {
|
||||
private String username;
|
||||
private String series;
|
||||
private String tokenValue;
|
||||
private Date date;
|
||||
|
||||
public PersistentRememberMeToken(String username, String series, String tokenValue, Date date) {
|
||||
this.username = username;
|
||||
this.series = series;
|
||||
this.tokenValue = tokenValue;
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getSeries() {
|
||||
return series;
|
||||
}
|
||||
|
||||
public String getTokenValue() {
|
||||
return tokenValue;
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return date;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
package org.springframework.security.ui.rememberme;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.springframework.security.Authentication;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* {@link RememberMeServices} implementation based on Barry Jaspan's
|
||||
* <a href="http://jaspan.com/improved_persistent_login_cookie_best_practice">Improved Persistent Login Cookie
|
||||
* Best Practice</a>.
|
||||
*
|
||||
* There is a slight modification to the described approach, in that the username is not stored as part of the cookie
|
||||
* but obtained from the persistent store via an implementation of {@link PersistentTokenRepository}. The latter
|
||||
* should place a unique constraint on the series identifier, so that it is impossible for the same identifier to be
|
||||
* allocated to two different users.
|
||||
*
|
||||
* <p>User management such as changing passwords, removing users and setting user status should be combined
|
||||
* with maintenance of the user's persistent tokens.
|
||||
* </p>
|
||||
*
|
||||
* <p>Note that while this class will use the date a token was created to check whether a presented cookie
|
||||
* is older than the configured <tt>tokenValiditySeconds</tt> property and deny authentication in this case,
|
||||
* it will to delete such tokens from the storage. A suitable batch process should be run periodically to
|
||||
* remove expired tokens from the database.
|
||||
* </p>
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
*/
|
||||
public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
|
||||
|
||||
private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
|
||||
private SecureRandom random;
|
||||
|
||||
public static final int DEFAULT_SERIES_LENGTH = 16;
|
||||
public static final int DEFAULT_TOKEN_LENGTH = 16;
|
||||
|
||||
private int seriesLength = DEFAULT_SERIES_LENGTH;
|
||||
private int tokenLength = DEFAULT_TOKEN_LENGTH;
|
||||
|
||||
public PersistentTokenBasedRememberMeServices() throws Exception {
|
||||
random = SecureRandom.getInstance("SHA1PRNG");
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the presented cookie data in the token repository, using the series id.
|
||||
* If the data compares successfully with that in the persistent store, a new token is generated and stored with
|
||||
* the same series. The corresponding cookie value is set on the response.
|
||||
*
|
||||
* @param cookieTokens the series and token values
|
||||
*
|
||||
* @throws RememberMeAuthenticationException if there is no stored token corresponding to the submitted cookie, or
|
||||
* if the token in the persistent store has expired.
|
||||
* @throws InvalidCookieException if the cookie doesn't have two tokens as expected.
|
||||
* @throws CookieTheftException if a presented series value is found, but the stored token is different from the
|
||||
* one presented.
|
||||
*/
|
||||
protected String processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
|
||||
|
||||
if (cookieTokens.length != 2) {
|
||||
throw new InvalidCookieException("Cookie token did not contain " + 2 +
|
||||
" tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
|
||||
}
|
||||
|
||||
final String presentedSeries = cookieTokens[0];
|
||||
final String presentedToken = cookieTokens[1];
|
||||
|
||||
PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries);
|
||||
|
||||
if (token == null) {
|
||||
// No series match, so we can't authenticate using this cookie
|
||||
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
|
||||
}
|
||||
|
||||
// We have a match for this user/series combination
|
||||
if (!presentedToken.equals(token.getTokenValue())) {
|
||||
// Token doesn't match series value. Delete all logins for this user and throw an exception to warn them.
|
||||
tokenRepository.removeAllTokens(token.getUsername());
|
||||
|
||||
throw new CookieTheftException(messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen",
|
||||
"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
|
||||
}
|
||||
|
||||
if (token.getDate().getTime() + getTokenValiditySeconds()*1000 < System.currentTimeMillis()) {
|
||||
throw new RememberMeAuthenticationException("Remember-me login has expired");
|
||||
}
|
||||
|
||||
// Token also matches, so login is valid. create and save new token with the *same* series number.
|
||||
PersistentRememberMeToken newToken = createNewToken(token.getUsername(), token.getSeries());
|
||||
|
||||
addCookie(newToken, request, response);
|
||||
|
||||
return token.getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new persistent login token with a new series number, stores the data in the
|
||||
* persistent token repository and adds the corresponding cookie to the response.
|
||||
*
|
||||
*/
|
||||
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
|
||||
PersistentRememberMeToken token = createNewToken(successfulAuthentication.getName(), null);
|
||||
addCookie(token, request, response);
|
||||
}
|
||||
|
||||
private PersistentRememberMeToken createNewToken(String username, String series) {
|
||||
logger.debug("Creating new persistent login token for user " + username);
|
||||
|
||||
if (series == null) {
|
||||
byte[] newSeries = new byte[seriesLength];
|
||||
random.nextBytes(newSeries);
|
||||
series = new String(Base64.encodeBase64(newSeries));
|
||||
logger.debug("New series: " + series);
|
||||
}
|
||||
|
||||
byte[] token = new byte[tokenLength];
|
||||
random.nextBytes(token);
|
||||
|
||||
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, series,
|
||||
new String(Base64.encodeBase64(token)), new Date());
|
||||
|
||||
tokenRepository.saveToken(persistentToken);
|
||||
|
||||
return persistentToken;
|
||||
}
|
||||
|
||||
private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
|
||||
String cookieValue = encodeCookie(new String[] {token.getSeries(), token.getTokenValue()});
|
||||
long maxAge = System.currentTimeMillis() + getTokenValiditySeconds() * 1000;
|
||||
response.addCookie(makeValidCookie(cookieValue, request, maxAge));
|
||||
}
|
||||
|
||||
public void setTokenRepository(PersistentTokenRepository tokenRepository) {
|
||||
this.tokenRepository = tokenRepository;
|
||||
}
|
||||
|
||||
public void setSeriesLength(int seriesLength) {
|
||||
this.seriesLength = seriesLength;
|
||||
}
|
||||
|
||||
public void setTokenLength(int tokenLength) {
|
||||
this.tokenLength = tokenLength;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package org.springframework.security.ui.rememberme;
|
||||
|
||||
/**
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
*/
|
||||
public interface PersistentTokenRepository {
|
||||
|
||||
void saveToken(PersistentRememberMeToken token);
|
||||
|
||||
PersistentRememberMeToken getTokenForSeries(String seriesId);
|
||||
|
||||
void removeAllTokens(String username);
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package org.springframework.security.ui.rememberme;
|
||||
|
||||
import org.springframework.security.AuthenticationException;
|
||||
|
||||
/**
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
*/
|
||||
public class RememberMeAuthenticationException extends AuthenticationException {
|
||||
|
||||
public RememberMeAuthenticationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -95,8 +95,7 @@ import org.springframework.web.bind.RequestUtils;
|
|||
* </p>
|
||||
*
|
||||
* @author Ben Alex
|
||||
* @version $Id: TokenBasedRememberMeServices.java 1871 2007-05-25 03:12:49Z
|
||||
* benalex $
|
||||
* @version $Id$
|
||||
*/
|
||||
public class TokenBasedRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
|
||||
//~ Static fields/initializers =====================================================================================
|
||||
|
|
|
@ -0,0 +1,277 @@
|
|||
package org.springframework.security.ui.rememberme;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.Authentication;
|
||||
import org.springframework.security.GrantedAuthority;
|
||||
import org.springframework.security.GrantedAuthorityImpl;
|
||||
import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.userdetails.User;
|
||||
import org.springframework.security.userdetails.UserDetails;
|
||||
import org.springframework.security.userdetails.UserDetailsService;
|
||||
import org.springframework.security.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
*/
|
||||
public class AbstractRememberMeServicesTests {
|
||||
User joe = new User("joe", "password", true, true,true,true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_A")});
|
||||
|
||||
@Test(expected = InvalidCookieException.class)
|
||||
public void nonBase64CookieShouldBeDetected() {
|
||||
new MockRememberMeServices().decodeCookie("nonBase64CookieValue%");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cookieShouldBeCorrectlyEncodedAndDecoded() {
|
||||
String[] cookie = new String[] {"the", "cookie", "tokens", "blah"};
|
||||
MockRememberMeServices services = new MockRememberMeServices();
|
||||
|
||||
String encoded = services.encodeCookie(cookie);
|
||||
// '=' aren't alowed in version 0 cookies.
|
||||
assertFalse(encoded.endsWith("="));
|
||||
String[] decoded = services.decodeCookie(encoded);
|
||||
|
||||
assertEquals(4, decoded.length);
|
||||
assertEquals("the", decoded[0]);
|
||||
assertEquals("cookie", decoded[1]);
|
||||
assertEquals("tokens", decoded[2]);
|
||||
assertEquals("blah", decoded[3]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void autoLoginShouldReturnNullIfNoLoginCookieIsPresented() {
|
||||
MockRememberMeServices services = new MockRememberMeServices();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
assertNull(services.autoLogin(request, response));
|
||||
|
||||
// shouldn't try to invalidate our cookie
|
||||
assertNull(response.getCookie(AbstractRememberMeServices.SPRING_SECURITY_PERSISTENT_REMEMBER_ME_COOKIE_KEY));
|
||||
|
||||
request = new MockHttpServletRequest();
|
||||
response = new MockHttpServletResponse();
|
||||
// set non-login cookie
|
||||
request.setCookies(new Cookie[] {new Cookie("mycookie", "cookie")});
|
||||
assertNull(services.autoLogin(request, response));
|
||||
assertNull(response.getCookie(AbstractRememberMeServices.SPRING_SECURITY_PERSISTENT_REMEMBER_ME_COOKIE_KEY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void successfulAutoLoginReturnsExpectedAuthentication() {
|
||||
MockRememberMeServices services = new MockRememberMeServices();
|
||||
services.setUserDetailsService(new MockAuthenticationDao(joe, false));
|
||||
assertNotNull(services.getUserDetailsService());
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
request.setCookies(createLoginCookie("cookie:1:2"));
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
Authentication result = services.autoLogin(request, response);
|
||||
|
||||
assertNotNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void autoLoginShouldFailIfInvalidCookieExceptionIsRaised() {
|
||||
MockRememberMeServices services = new MockRememberMeServices();
|
||||
services.setUserDetailsService(new MockAuthenticationDao(joe, true));
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
// Wrong number of tokes
|
||||
request.setCookies(createLoginCookie("cookie:1"));
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
Authentication result = services.autoLogin(request, response);
|
||||
|
||||
assertNull(result);
|
||||
|
||||
assertCookieCancelled(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void autoLoginShouldFailIfUserNotFound() {
|
||||
MockRememberMeServices services = new MockRememberMeServices();
|
||||
services.setUserDetailsService(new MockAuthenticationDao(joe, true));
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setCookies(createLoginCookie("cookie:1:2"));
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
Authentication result = services.autoLogin(request, response);
|
||||
|
||||
assertNull(result);
|
||||
|
||||
assertCookieCancelled(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void autoLoginShouldFailIfUserAccountIsLocked() {
|
||||
MockRememberMeServices services = new MockRememberMeServices();
|
||||
User joeLocked = new User("joe", "password",false,true,true,true,joe.getAuthorities());
|
||||
services.setUserDetailsService(new MockAuthenticationDao(joeLocked, false));
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setCookies(createLoginCookie("cookie:1:2"));
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
Authentication result = services.autoLogin(request, response);
|
||||
|
||||
assertNull(result);
|
||||
|
||||
assertCookieCancelled(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginFailShouldCancelCookie() {
|
||||
MockRememberMeServices services = new MockRememberMeServices();
|
||||
services.setUserDetailsService(new MockAuthenticationDao(joe, true));
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setContextPath("contextpath");
|
||||
request.setCookies(createLoginCookie("cookie:1:2"));
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
services.loginFail(request, response);
|
||||
|
||||
assertCookieCancelled(response);
|
||||
}
|
||||
|
||||
@Test(expected = CookieTheftException.class)
|
||||
public void cookieTheftExceptionShouldBeRethrown() {
|
||||
MockRememberMeServices services = new MockRememberMeServices() {
|
||||
protected String processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
|
||||
throw new CookieTheftException("Pretending cookie was stolen");
|
||||
}
|
||||
};
|
||||
|
||||
services.setUserDetailsService(new MockAuthenticationDao(joe, false));
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
request.setCookies(createLoginCookie("cookie:1:2"));
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
services.autoLogin(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginSuccessCallsOnLoginSuccessCorrectly() {
|
||||
MockRememberMeServices services = new MockRememberMeServices();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
Authentication auth = new UsernamePasswordAuthenticationToken("joe","password");
|
||||
|
||||
// No parameter set
|
||||
services = new MockRememberMeServices();
|
||||
services.loginSuccess(request, response, auth);
|
||||
assertFalse(services.loginSuccessCalled);
|
||||
|
||||
// Parameter set to true
|
||||
services = new MockRememberMeServices();
|
||||
request.setParameter(MockRememberMeServices.DEFAULT_PARAMETER, "true");
|
||||
services.loginSuccess(request, response, auth);
|
||||
assertTrue(services.loginSuccessCalled);
|
||||
|
||||
// Different parameter name, set to true
|
||||
services = new MockRememberMeServices();
|
||||
services.setParameter("my_parameter");
|
||||
request.setParameter("my_parameter", "true");
|
||||
services.loginSuccess(request, response, auth);
|
||||
assertTrue(services.loginSuccessCalled);
|
||||
|
||||
|
||||
// Parameter set to false
|
||||
services = new MockRememberMeServices();
|
||||
request.setParameter(MockRememberMeServices.DEFAULT_PARAMETER, "false");
|
||||
services.loginSuccess(request, response, auth);
|
||||
assertFalse(services.loginSuccessCalled);
|
||||
|
||||
// alwaysRemember set to true
|
||||
services = new MockRememberMeServices();
|
||||
services.setAlwaysRemember(true);
|
||||
services.loginSuccess(request, response, auth);
|
||||
assertTrue(services.loginSuccessCalled);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void makeValidCookieUsesCorrectNamePathAndValue() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setContextPath("contextpath");
|
||||
MockRememberMeServices services = new MockRememberMeServices();
|
||||
services.setCookieName("mycookiename");
|
||||
Cookie cookie = services.makeValidCookie("mycookie", request, 1000);
|
||||
|
||||
assertTrue(cookie.getValue().equals("mycookie"));
|
||||
assertTrue(cookie.getName().equals("mycookiename"));
|
||||
assertTrue(cookie.getPath().equals("contextpath"));
|
||||
|
||||
}
|
||||
|
||||
|
||||
private Cookie[] createLoginCookie(String cookieToken) {
|
||||
MockRememberMeServices services = new MockRememberMeServices();
|
||||
Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_PERSISTENT_REMEMBER_ME_COOKIE_KEY,
|
||||
services.encodeCookie(StringUtils.delimitedListToStringArray(cookieToken, ":")));
|
||||
|
||||
return new Cookie[] {cookie};
|
||||
}
|
||||
|
||||
private void assertCookieCancelled(MockHttpServletResponse response) {
|
||||
Cookie returnedCookie = response.getCookie(AbstractRememberMeServices.SPRING_SECURITY_PERSISTENT_REMEMBER_ME_COOKIE_KEY);
|
||||
assertNotNull(returnedCookie);
|
||||
assertEquals(0, returnedCookie.getMaxAge());
|
||||
}
|
||||
|
||||
//~ Inner Classes ==================================================================================================
|
||||
|
||||
private class MockRememberMeServices extends AbstractRememberMeServices {
|
||||
boolean loginSuccessCalled;
|
||||
|
||||
private MockRememberMeServices() {
|
||||
setKey("key");
|
||||
}
|
||||
|
||||
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
|
||||
loginSuccessCalled = true;
|
||||
}
|
||||
|
||||
protected String processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) throws RememberMeAuthenticationException {
|
||||
if(cookieTokens.length != 3) {
|
||||
throw new InvalidCookieException("deliberate exception");
|
||||
}
|
||||
|
||||
return "joe";
|
||||
}
|
||||
}
|
||||
|
||||
private class MockAuthenticationDao implements UserDetailsService {
|
||||
private UserDetails toReturn;
|
||||
private boolean throwException;
|
||||
|
||||
public MockAuthenticationDao(UserDetails toReturn, boolean throwException) {
|
||||
this.toReturn = toReturn;
|
||||
this.throwException = throwException;
|
||||
}
|
||||
|
||||
public UserDetails loadUserByUsername(String username) {
|
||||
if (throwException) {
|
||||
throw new UsernameNotFoundException("as requested by mock");
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package org.springframework.security.ui.rememberme;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
*/
|
||||
public class PersistentTokenBasedRememberMeServicesTests {
|
||||
private PersistentTokenBasedRememberMeServices services;
|
||||
|
||||
@Before
|
||||
public void setUpData() throws Exception {
|
||||
services = new PersistentTokenBasedRememberMeServices();
|
||||
}
|
||||
|
||||
@Test(expected = InvalidCookieException.class)
|
||||
public void loginIsRejectedWithWrongNumberOfCookieTokens() {
|
||||
services.setCookieName("mycookiename");
|
||||
services.processAutoLoginCookie(new String[] {"series", "token", "extra"}, new MockHttpServletRequest(),
|
||||
new MockHttpServletResponse());
|
||||
}
|
||||
|
||||
@Test(expected = RememberMeAuthenticationException.class)
|
||||
public void loginIsRejectedWhenNoTokenMatchingSeriesIsFound() {
|
||||
services.setCookieName("mycookiename");
|
||||
services.setTokenRepository(new MockTokenRepository(null));
|
||||
services.processAutoLoginCookie(new String[] {"series", "token"}, new MockHttpServletRequest(),
|
||||
new MockHttpServletResponse());
|
||||
}
|
||||
|
||||
@Test(expected = CookieTheftException.class)
|
||||
public void cookieTheftIsDetectedWhenSeriesAndTokenDontMatch() {
|
||||
services.setCookieName("mycookiename");
|
||||
PersistentRememberMeToken token = new PersistentRememberMeToken("joe", "series","wrongtoken", new Date());
|
||||
services.setTokenRepository(new MockTokenRepository(token));
|
||||
services.processAutoLoginCookie(new String[] {"series", "token"}, new MockHttpServletRequest(),
|
||||
new MockHttpServletResponse());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void successfulAutoLoginCreatesNewTokenAndCookieWithSameSeries() {
|
||||
services.setCookieName("mycookiename");
|
||||
MockTokenRepository repo =
|
||||
new MockTokenRepository(new PersistentRememberMeToken("joe", "series","token", new Date()));
|
||||
services.setTokenRepository(repo);
|
||||
// 12 => b64 length will be 16
|
||||
services.setTokenLength(12);
|
||||
services.processAutoLoginCookie(new String[] {"series", "token"}, new MockHttpServletRequest(),
|
||||
new MockHttpServletResponse());
|
||||
assertEquals("series",repo.getStoredToken().getSeries());
|
||||
assertEquals(16, repo.getStoredToken().getTokenValue().length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginSuccessCreatesNewTokenAndCookieWithNewSeries() {
|
||||
services.setAlwaysRemember(true);
|
||||
MockTokenRepository repo = new MockTokenRepository(null);
|
||||
services.setTokenRepository(repo);
|
||||
services.setTokenLength(12);
|
||||
services.setSeriesLength(12);
|
||||
services.loginSuccess(new MockHttpServletRequest(),
|
||||
new MockHttpServletResponse(), new UsernamePasswordAuthenticationToken("joe","password"));
|
||||
assertEquals(16, repo.getStoredToken().getSeries().length());
|
||||
assertEquals(16, repo.getStoredToken().getTokenValue().length());
|
||||
}
|
||||
|
||||
|
||||
|
||||
private class MockTokenRepository implements PersistentTokenRepository {
|
||||
private PersistentRememberMeToken storedToken;
|
||||
|
||||
private MockTokenRepository(PersistentRememberMeToken token) {
|
||||
storedToken = token;
|
||||
}
|
||||
|
||||
public void saveToken(PersistentRememberMeToken token) {
|
||||
storedToken = token;
|
||||
}
|
||||
|
||||
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
|
||||
return storedToken;
|
||||
}
|
||||
|
||||
public void removeAllTokens(String username) {
|
||||
}
|
||||
|
||||
PersistentRememberMeToken getStoredToken() {
|
||||
return storedToken;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue