View Javadoc

1   package org.springframework.security.ui.rememberme;
2   
3   import org.springframework.security.Authentication;
4   import org.springframework.security.userdetails.UserDetails;
5   import org.springframework.dao.DataAccessException;
6   
7   import org.apache.commons.codec.binary.Base64;
8   
9   import javax.servlet.http.HttpServletRequest;
10  import javax.servlet.http.HttpServletResponse;
11  import java.security.SecureRandom;
12  import java.util.Arrays;
13  import java.util.Date;
14  
15  /**
16   * {@link RememberMeServices} implementation based on Barry Jaspan's
17   * <a href="http://jaspan.com/improved_persistent_login_cookie_best_practice">Improved Persistent Login Cookie
18   * Best Practice</a>.
19   *
20   * There is a slight modification to the described approach, in that the username is not stored as part of the cookie
21   * but obtained from the persistent store via an implementation of {@link PersistentTokenRepository}. The latter
22   * should place a unique constraint on the series identifier, so that it is impossible for the same identifier to be
23   * allocated to two different users.
24   *
25   * <p>User management such as changing passwords, removing users and setting user status should be combined
26   * with maintenance of the user's persistent tokens.
27   * </p>
28   *
29   * <p>Note that while this class will use the date a token was created to check whether a presented cookie
30   * is older than the configured <tt>tokenValiditySeconds</tt> property and deny authentication in this case,
31   * it will to delete such tokens from the storage. A suitable batch process should be run periodically to
32   * remove expired tokens from the database.
33   * </p>
34   *
35   * @author Luke Taylor
36   * @version $Id: PersistentTokenBasedRememberMeServices.java 2592 2008-02-04 21:26:07Z luke_t $
37   * @since 2.0
38   */
39  public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
40  
41      private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
42      private SecureRandom random;
43  
44      public static final int DEFAULT_SERIES_LENGTH = 16;
45      public static final int DEFAULT_TOKEN_LENGTH = 16;
46  
47      private int seriesLength = DEFAULT_SERIES_LENGTH;
48      private int tokenLength = DEFAULT_TOKEN_LENGTH;
49  
50      public PersistentTokenBasedRememberMeServices() throws Exception {
51          random = SecureRandom.getInstance("SHA1PRNG");
52      }
53  
54      /**
55       * Locates the presented cookie data in the token repository, using the series id.
56       * If the data compares successfully with that in the persistent store, a new token is generated and stored with
57       * the same series. The corresponding cookie value is set on the response.
58       *
59       * @param cookieTokens the series and token values
60       *
61       * @throws RememberMeAuthenticationException if there is no stored token corresponding to the submitted cookie, or
62       * if the token in the persistent store has expired.
63       * @throws InvalidCookieException if the cookie doesn't have two tokens as expected.
64       * @throws CookieTheftException if a presented series value is found, but the stored token is different from the
65       * one presented.
66       */
67      protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
68  
69          if (cookieTokens.length != 2) {
70              throw new InvalidCookieException("Cookie token did not contain " + 2 +
71                      " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
72          }
73  
74          final String presentedSeries = cookieTokens[0];
75          final String presentedToken = cookieTokens[1];
76  
77          PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries);
78  
79          if (token == null) {
80              // No series match, so we can't authenticate using this cookie
81              throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
82          }
83  
84          // We have a match for this user/series combination
85          if (!presentedToken.equals(token.getTokenValue())) {
86              // Token doesn't match series value. Delete all logins for this user and throw an exception to warn them.
87              tokenRepository.removeUserTokens(token.getUsername());
88  
89              throw new CookieTheftException(messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen",
90                      "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
91          }
92  
93          if (token.getDate().getTime() + getTokenValiditySeconds()*1000 < System.currentTimeMillis()) {
94              throw new RememberMeAuthenticationException("Remember-me login has expired");
95          }
96  
97          // Token also matches, so login is valid. Update the token value, keeping the *same* series number.
98          if (logger.isDebugEnabled()) {
99              logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" +
100                     token.getSeries() + "'");
101         }
102 
103         PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(),
104                 token.getSeries(), generateTokenData(), new Date());
105 
106         try {
107             tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
108             addCookie(newToken, request, response);
109         } catch (DataAccessException e) {
110             logger.error("Failed to update token: ", e);
111             throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
112         }
113 
114         UserDetails user = getUserDetailsService().loadUserByUsername(token.getUsername());
115 
116         return user;
117     }
118 
119     /**
120      * Creates a new persistent login token with a new series number, stores the data in the
121      * persistent token repository and adds the corresponding cookie to the response.
122      *
123      */
124     protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
125         String username = successfulAuthentication.getName();
126 
127         logger.debug("Creating new persistent login for user " + username);
128 
129         PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(),
130                 generateTokenData(), new Date());
131         try {
132             tokenRepository.createNewToken(persistentToken);
133             addCookie(persistentToken, request, response);
134         } catch (DataAccessException e) {
135             logger.error("Failed to save persistent token ", e);
136 
137         }
138     }
139 
140     protected String generateSeriesData() {
141         byte[] newSeries = new byte[seriesLength];
142         random.nextBytes(newSeries);
143         return new String(Base64.encodeBase64(newSeries));
144     }
145 
146     protected String generateTokenData() {
147         byte[] newToken = new byte[tokenLength];
148         random.nextBytes(newToken);
149         return new String(Base64.encodeBase64(newToken));
150     }
151 
152     private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
153         setCookie(new String[] {token.getSeries(), token.getTokenValue()},getTokenValiditySeconds(), request, response);
154     }
155 
156     public void setTokenRepository(PersistentTokenRepository tokenRepository) {
157         this.tokenRepository = tokenRepository;
158     }
159 
160     public void setSeriesLength(int seriesLength) {
161         this.seriesLength = seriesLength;
162     }
163 
164     public void setTokenLength(int tokenLength) {
165         this.tokenLength = tokenLength;
166     }
167 }