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    }