View Javadoc

1   package org.springframework.security.ui.rememberme;
2   
3   import org.apache.commons.codec.binary.Base64;
4   import org.apache.commons.logging.Log;
5   import org.apache.commons.logging.LogFactory;
6   import org.springframework.beans.factory.InitializingBean;
7   import org.springframework.context.support.MessageSourceAccessor;
8   import org.springframework.security.Authentication;
9   import org.springframework.security.SpringSecurityMessageSource;
10  import org.springframework.security.AccountStatusException;
11  import org.springframework.security.providers.rememberme.RememberMeAuthenticationToken;
12  import org.springframework.security.ui.AuthenticationDetailsSource;
13  import org.springframework.security.ui.WebAuthenticationDetailsSource;
14  import org.springframework.security.ui.logout.LogoutHandler;
15  import org.springframework.security.userdetails.UserDetails;
16  import org.springframework.security.userdetails.UserDetailsService;
17  import org.springframework.security.userdetails.UsernameNotFoundException;
18  import org.springframework.security.userdetails.UserDetailsChecker;
19  import org.springframework.security.userdetails.checker.AccountStatusUserDetailsChecker;
20  import org.springframework.util.Assert;
21  import org.springframework.util.StringUtils;
22  
23  import javax.servlet.http.Cookie;
24  import javax.servlet.http.HttpServletRequest;
25  import javax.servlet.http.HttpServletResponse;
26  
27  /**
28   * Base class for RememberMeServices implementations.
29   *
30   * @author Luke Taylor
31   * @version $Id: AbstractRememberMeServices.java 3268 2008-09-04 16:05:34Z luke_t $
32   * @since 2.0
33   */
34  public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
35      //~ Static fields/initializers =====================================================================================
36  
37      public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = "SPRING_SECURITY_REMEMBER_ME_COOKIE";
38      public static final String DEFAULT_PARAMETER = "_spring_security_remember_me";
39  
40      private static final String DELIMITER = ":";
41  
42      //~ Instance fields ================================================================================================
43      protected final Log logger = LogFactory.getLog(getClass());
44  
45      protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
46  
47      private UserDetailsService userDetailsService;
48      private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
49      private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
50  
51      private String cookieName = SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY;
52      private String parameter = DEFAULT_PARAMETER;
53      private boolean alwaysRemember;
54      private String key;
55      private int tokenValiditySeconds = 1209600; // 14 days
56  
57      public void afterPropertiesSet() throws Exception {
58          Assert.hasLength(key);
59          Assert.hasLength(parameter);
60          Assert.hasLength(cookieName);
61          Assert.notNull(userDetailsService);
62      }
63  
64      /**
65       * Template implementation which locates the Spring Security cookie, decodes it into
66       * a delimited array of tokens and submits it to subclasses for processing
67       * via the <tt>processAutoLoginCookie</tt> method.
68       * <p>
69       * The returned username is then used to load the UserDetails object for the user, which in turn
70       * is used to create a valid authentication token.
71       */
72      public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
73          String rememberMeCookie = extractRememberMeCookie(request);
74  
75          if (rememberMeCookie == null) {
76              return null;
77          }
78  
79          logger.debug("Remember-me cookie detected");
80  
81          UserDetails user = null;
82  
83          try {
84              String[] cookieTokens = decodeCookie(rememberMeCookie);
85              user = processAutoLoginCookie(cookieTokens, request, response);
86              userDetailsChecker.check(user);
87          } catch (CookieTheftException cte) {
88              cancelCookie(request, response);
89              throw cte;
90          } catch (UsernameNotFoundException noUser) {
91              cancelCookie(request, response);
92              logger.debug("Remember-me login was valid but corresponding user not found.", noUser);
93              return null;
94          } catch (InvalidCookieException invalidCookie) {
95              cancelCookie(request, response);
96              logger.debug("Invalid remember-me cookie: " + invalidCookie.getMessage());
97              return null;
98          } catch (AccountStatusException statusInvalid) {
99              cancelCookie(request, response);
100             logger.debug("Invalid UserDetails: " + statusInvalid.getMessage());
101             return null;
102         } catch (RememberMeAuthenticationException e) {
103             cancelCookie(request, response);
104             logger.debug(e.getMessage());
105             return null;
106         }
107 
108         logger.debug("Remember-me cookie accepted");
109 
110         RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(key, user, user.getAuthorities());
111         auth.setDetails(authenticationDetailsSource.buildDetails(request));
112 
113         return auth;
114     }
115 
116     /**
117      * Locates the Spring Security remember me cookie in the request.
118      *
119      * @param request the submitted request which is to be authenticated
120      * @return the cookie value (if present), null otherwise.
121      */
122     private String extractRememberMeCookie(HttpServletRequest request) {
123         Cookie[] cookies = request.getCookies();
124 
125         if ((cookies == null) || (cookies.length == 0)) {
126             return null;
127         }
128 
129         for (int i = 0; i < cookies.length; i++) {
130             if (cookieName.equals(cookies[i].getName())) {
131                 return cookies[i].getValue();
132             }
133         }
134 
135         return null;
136     }
137 
138     /**
139      * Decodes the cookie and splits it into a set of token strings using the ":" delimiter.
140      *
141      * @param cookieValue the value obtained from the submitted cookie
142      * @return the array of tokens.
143      * @throws InvalidCookieException if the cookie was not base64 encoded.
144      */
145     protected String[] decodeCookie(String cookieValue) throws InvalidCookieException {
146         for (int j = 0; j < cookieValue.length() % 4; j++) {
147             cookieValue = cookieValue + "=";
148         }
149 
150         if (!Base64.isArrayByteBase64(cookieValue.getBytes())) {
151             throw new InvalidCookieException( "Cookie token was not Base64 encoded; value was '" + cookieValue + "'");
152         }
153 
154         String cookieAsPlainText = new String(Base64.decodeBase64(cookieValue.getBytes()));
155 
156         return StringUtils.delimitedListToStringArray(cookieAsPlainText, DELIMITER);
157     }
158 
159     /**
160      * Inverse operation of decodeCookie.
161      *
162      * @param cookieTokens the tokens to be encoded.
163      * @return base64 encoding of the tokens concatenated with the ":" delimiter.
164      */
165     protected String encodeCookie(String[] cookieTokens) {
166         StringBuffer sb = new StringBuffer();
167         for(int i=0; i < cookieTokens.length; i++) {
168             sb.append(cookieTokens[i]);
169 
170             if (i < cookieTokens.length - 1) {
171                 sb.append(DELIMITER);
172             }
173         }
174 
175         String value = sb.toString();
176 
177         sb = new StringBuffer(new String(Base64.encodeBase64(value.getBytes())));
178 
179         while (sb.charAt(sb.length() - 1) == '=') {
180             sb.deleteCharAt(sb.length() - 1);
181         }
182 
183         return sb.toString();
184     }
185 
186     public final void loginFail(HttpServletRequest request, HttpServletResponse response) {
187         logger.debug("Interactive login attempt was unsuccessful.");
188         cancelCookie(request, response);
189         onLoginFail(request, response);
190     }
191 
192     protected void onLoginFail(HttpServletRequest request, HttpServletResponse response) {}
193 
194     /**
195      * Examines the incoming request and checks for the presence of the configured "remember me" parameter.
196      * If it's present, or if <tt>alwaysRemember</tt> is set to true, calls <tt>onLoginSucces</tt>.
197      */
198     public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
199             Authentication successfulAuthentication) {
200 
201         if (!rememberMeRequested(request, parameter)) {
202             logger.debug("Remember-me login not requested.");
203             return;
204         }
205 
206         onLoginSuccess(request, response, successfulAuthentication);
207     }
208 
209     /**
210      * Called from loginSuccess when a remember-me login has been requested.
211      * Typically implemented by subclasses to set a remember-me cookie and potentially store a record
212      * of it if the implementation requires this.
213      */
214     protected abstract void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
215             Authentication successfulAuthentication);
216 
217     /**
218      * Allows customization of whether a remember-me login has been requested.
219      * The default is to return true if <tt>alwaysRemember</tt> is set or the configured parameter name has
220      * been included in the request and is set to the value "true".
221      *
222      * @param request the request submitted from an interactive login, which may include additional information
223      * indicating that a persistent login is desired.
224      * @param parameter the configured remember-me parameter name.
225      *
226      * @return true if the request includes information indicating that a persistent login has been
227      * requested.
228      */
229     protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
230         if (alwaysRemember) {
231             return true;
232         }
233 
234         String paramValue = request.getParameter(parameter);
235 
236         if (paramValue != null) {
237             if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") ||
238                     paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
239                 return true;
240             }
241         }
242 
243         if (logger.isDebugEnabled()) {
244             logger.debug("Did not send remember-me cookie (principal did not set parameter '" + parameter + "')");
245         }
246 
247         return false;
248     }
249 
250     /**
251      * Called from autoLogin to process the submitted persistent login cookie. Subclasses should
252      * validate the cookie and perform any additional management required.
253      *
254      * @param cookieTokens the decoded and tokenized cookie value
255      * @param request the request
256      * @param response the response, to allow the cookie to be modified if required.
257      * @return the UserDetails for the corresponding user account if the cookie was validated successfully.
258      * @throws RememberMeAuthenticationException if the cookie is invalid or the login is invalid for some
259      * other reason.
260      * @throws UsernameNotFoundException if the user account corresponding to the login cookie couldn't be found
261      * (for example if the user has been removed from the system).
262      */
263     protected abstract UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
264             HttpServletResponse response) throws RememberMeAuthenticationException, UsernameNotFoundException;
265 
266     /**
267      * Sets a "cancel cookie" (with maxAge = 0) on the response to disable persistent logins.
268      *
269      * @param request
270      * @param response
271      */
272     protected void cancelCookie(HttpServletRequest request, HttpServletResponse response) {
273         logger.debug("Cancelling cookie");
274         Cookie cookie = new Cookie(cookieName, null);
275         cookie.setMaxAge(0);
276         cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
277 
278         response.addCookie(cookie);
279     }
280 
281     /**
282      * Sets the cookie on the response
283      *
284      * @param tokens the tokens which will be encoded to make the cookie value.
285      * @param maxAge the value passed to {@link Cookie#setMaxAge(int)}
286      * @param request the request
287      * @param response the response to add the cookie to.
288      */
289     protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request, HttpServletResponse response) {
290         String cookieValue = encodeCookie(tokens);
291         Cookie cookie = new Cookie(cookieName, cookieValue);
292         cookie.setMaxAge(maxAge);
293         cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
294         response.addCookie(cookie);
295     }
296 
297     /**
298      * Implementation of <tt>LogoutHandler</tt>. Default behaviour is to call <tt>cancelCookie()</tt>.
299      */
300     public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
301         if (logger.isDebugEnabled()) {
302             logger.debug( "Logout of user "
303                     + (authentication == null ? "Unknown" : authentication.getName()));
304         }
305         cancelCookie(request, response);
306     }
307 
308     public void setCookieName(String cookieName) {
309         this.cookieName = cookieName;
310     }
311 
312     protected String getCookieName() {
313         return cookieName;
314     }
315 
316     public void setAlwaysRemember(boolean alwaysRemember) {
317         this.alwaysRemember = alwaysRemember;
318     }
319 
320     /**
321      * Sets the name of the parameter which should be checked for to see if a remember-me has been requested
322      * during a login request. This should be the same name you assign to the checkbox in your login form.
323      *
324      * @param parameter the HTTP request parameter
325      */
326     public void setParameter(String parameter) {
327         Assert.hasText(parameter, "Parameter name cannot be null");
328         this.parameter = parameter;
329     }
330 
331     public String getParameter() {
332         return parameter;
333     }
334 
335     protected UserDetailsService getUserDetailsService() {
336         return userDetailsService;
337     }
338 
339     public void setUserDetailsService(UserDetailsService userDetailsService) {
340         Assert.notNull(userDetailsService, "UserDetailsService canot be null");
341         this.userDetailsService = userDetailsService;
342     }
343 
344     public void setKey(String key) {
345         this.key = key;
346     }
347 
348     public String getKey() {
349         return key;
350     }
351 
352     public void setTokenValiditySeconds(int tokenValiditySeconds) {
353         this.tokenValiditySeconds = tokenValiditySeconds;
354     }
355 
356     protected int getTokenValiditySeconds() {
357         return tokenValiditySeconds;
358     }
359 
360     protected AuthenticationDetailsSource getAuthenticationDetailsSource() {
361         return authenticationDetailsSource;
362     }
363 
364     public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
365         Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource cannot be null");
366         this.authenticationDetailsSource = authenticationDetailsSource;
367     }
368 }