001 package org.trails.security;
002
003 import java.math.BigInteger;
004 import java.util.Date;
005 import java.util.List;
006 import java.util.Random;
007
008 import javax.servlet.http.Cookie;
009 import javax.servlet.http.HttpServletRequest;
010 import javax.servlet.http.HttpServletResponse;
011
012 import org.acegisecurity.Authentication;
013 import org.acegisecurity.ui.rememberme.RememberMeServices;
014 import org.apache.commons.logging.Log;
015 import org.apache.commons.logging.LogFactory;
016 import org.hibernate.criterion.DetachedCriteria;
017 import org.hibernate.criterion.Restrictions;
018 import org.trails.persistence.HibernatePersistenceService;
019
020 public class RollingCookieRememberMeServices implements RememberMeServices {
021 private static final Log log = LogFactory.getLog(RollingCookieRememberMeServices.class );
022
023 private static Random random = new Random((new Date()).getTime() );
024 private enum Keys{j_rememberme, remembermetoken}
025 HibernatePersistenceService persistenceService;
026
027 private char separatorChar = '-';
028 // In seconds, default is a month
029 private int maxAge = 30 * 24 * 3600;
030
031 public int getMaxAge() {
032 return maxAge;
033 }
034
035 public void setMaxAge(int maxAge) {
036 this.maxAge = maxAge;
037 }
038
039 public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
040 Cookie[] cookies = request.getCookies();
041 if ((cookies == null) || (cookies.length == 0)) return null;
042 for (Cookie cookie : cookies) if (Keys.remembermetoken.name().equals(cookie.getName()) ) {
043 String cookieValue = cookie.getValue();
044 int separatorPos = cookieValue.indexOf(separatorChar );
045 if (separatorPos <= 0) return null;
046 //if (!cookie.getPath().equals(request.getContextPath())) return null;
047 log.info("Trying to remember user from " + request.getRemoteAddr() + " with credentials " + cookieValue);
048 return new UserKeyAuthenticationToken(cookieValue.substring(separatorPos+1), cookieValue.substring(0, separatorPos) );
049 }
050 return null;
051 }
052
053 public void loginFail(HttpServletRequest request, HttpServletResponse response) {
054 clearRememberMeCookie(request.getContextPath(), response);
055 }
056
057 private String createExpiringKeyForUser(String username) {
058 // purge expired tokens here so we don't need to do it periodically - ok to do this everytime?
059 try {
060 DetachedCriteria detachedCriteria = DetachedCriteria.forClass(ExpiringKey.class);
061 detachedCriteria.add(Restrictions.eq("name", username) );
062 detachedCriteria.add(Restrictions.lt("expiresAfter", new Date()) );
063 List<ExpiringKey> credentials = persistenceService.getInstances(ExpiringKey.class, detachedCriteria );
064 persistenceService.removeAll(credentials);
065 }
066 catch (Exception e) {
067 log.warn("Purging expired credentials failed because of: " + e.getMessage() );
068 }
069
070 ExpiringKey expiringKey = new ExpiringKey(username, (new BigInteger(128, random)).toString(), new Date((new Date()).getTime() + maxAge * 1000L) );
071 persistenceService.save(expiringKey);
072 return expiringKey.getValue() + separatorChar + expiringKey.getName();
073 }
074
075 public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
076 if (log.isTraceEnabled()) log.trace("j_rememberme is " + request.getParameter(Keys.j_rememberme.name()) );
077
078 if (request.getParameter(Keys.j_rememberme.name()) == null && !(authentication instanceof UserKeyAuthenticationToken) ) return;
079
080 if (authentication instanceof UserKeyAuthenticationToken) try {
081 // Rolling tokens, remove the used one
082 // TODO This performs slowly, would be better to add a HQL pass-through method in persistence service
083 DetachedCriteria detachedCriteria = DetachedCriteria.forClass(ExpiringKey.class);
084 detachedCriteria.add(Restrictions.eq("name", authentication.getName() ) );
085 detachedCriteria.add(Restrictions.eq("value", authentication.getCredentials() ) );
086 List<ExpiringKey> credentials = persistenceService.getInstances(ExpiringKey.class, detachedCriteria );
087
088 // persistenceService.removeAll(credentials) won't work. When remember me is used, there may be several incoming requests that are sending the same
089 // token as credentials. If we invalidate the token after the first request, the second request fails, the request
090 // is redirected to the the login page, but that request is authenticated with the new cookie and so we go to
091 // an infinite loop. Instead, make the credentials expire soon (for example less than session timeout)
092 // Assume there's only one credential in the list. Expired credentials will be cleaned up later in any case
093 if (credentials.size() > 0) {
094 ExpiringKey credential = credentials.get(0);
095 // Expire in one min
096 credential.setExpiresAfter(new Date((new Date()).getTime() + 60000L));
097 persistenceService.save(credential);
098 }
099 }
100 catch (Exception e) {
101 log.warn("Couldn't expire used credentials because of " + e.getMessage() );
102 }
103
104 String username = authentication.getName();
105 Cookie cookie = new Cookie(Keys.remembermetoken.name(), createExpiringKeyForUser(username).toString() );
106 cookie.setPath(request.getContextPath());
107 cookie.setMaxAge(maxAge);
108 response.addCookie(cookie);
109 }
110
111 public void setPersistenceService(HibernatePersistenceService persistenceService)
112 {
113 this.persistenceService = persistenceService;
114 }
115
116 public char getSeparatorChar() {
117 return separatorChar;
118 }
119
120 public void setSeparatorChar(char separatorChar) {
121 this.separatorChar = separatorChar;
122 }
123
124 public static void clearRememberMeCookie(String contextPath, HttpServletResponse response) {
125 Cookie cookie = new Cookie(Keys.remembermetoken.name(), "");
126 cookie.setPath(contextPath == null ? "/" : contextPath);
127 cookie.setMaxAge(0);
128 response.addCookie(cookie);
129 }
130 }