11 changed files with 964 additions and 2 deletions
@ -0,0 +1,306 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,277 @@
@@ -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 @@
@@ -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