Coverage Report - org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices
 
Classes in this File Line Coverage Branch Coverage Complexity
TokenBasedRememberMeServices
77% 
86% 
2.467
 
 1  
 /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 2  
  *
 3  
  * Licensed under the Apache License, Version 2.0 (the "License");
 4  
  * you may not use this file except in compliance with the License.
 5  
  * You may obtain a copy of the License at
 6  
  *
 7  
  *     http://www.apache.org/licenses/LICENSE-2.0
 8  
  *
 9  
  * Unless required by applicable law or agreed to in writing, software
 10  
  * distributed under the License is distributed on an "AS IS" BASIS,
 11  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12  
  * See the License for the specific language governing permissions and
 13  
  * limitations under the License.
 14  
  */
 15  
 
 16  
 package org.acegisecurity.ui.rememberme;
 17  
 
 18  
 import java.util.Date;
 19  
 import java.util.Map;
 20  
 
 21  
 import javax.servlet.http.Cookie;
 22  
 import javax.servlet.http.HttpServletRequest;
 23  
 import javax.servlet.http.HttpServletResponse;
 24  
 
 25  
 import org.acegisecurity.Authentication;
 26  
 import org.acegisecurity.providers.rememberme.RememberMeAuthenticationToken;
 27  
 import org.acegisecurity.ui.AccessDeniedHandler;
 28  
 import org.acegisecurity.ui.AuthenticationDetailsSource;
 29  
 import org.acegisecurity.ui.AuthenticationDetailsSourceImpl;
 30  
 import org.acegisecurity.ui.logout.LogoutHandler;
 31  
 import org.acegisecurity.userdetails.UserDetails;
 32  
 import org.acegisecurity.userdetails.UserDetailsService;
 33  
 import org.acegisecurity.userdetails.UsernameNotFoundException;
 34  
 import org.apache.commons.codec.binary.Base64;
 35  
 import org.apache.commons.codec.digest.DigestUtils;
 36  
 import org.apache.commons.logging.Log;
 37  
 import org.apache.commons.logging.LogFactory;
 38  
 import org.springframework.beans.factory.InitializingBean;
 39  
 import org.springframework.context.ApplicationContext;
 40  
 import org.springframework.util.Assert;
 41  
 import org.springframework.util.StringUtils;
 42  
 import org.springframework.web.bind.RequestUtils;
 43  
 
 44  
 /**
 45  
  * Identifies previously remembered users by a Base-64 encoded cookie.
 46  
  * 
 47  
  * <p>
 48  
  * This implementation does not rely on an external database, so is attractive
 49  
  * for simple applications. The cookie will be valid for a specific period from
 50  
  * the date of the last
 51  
  * {@link #loginSuccess(HttpServletRequest, HttpServletResponse, Authentication)}.
 52  
  * As per the interface contract, this method will only be called when the
 53  
  * principal completes a successful interactive authentication. As such the time
 54  
  * period commences from the last authentication attempt where they furnished
 55  
  * credentials - not the time period they last logged in via remember-me. The
 56  
  * implementation will only send a remember-me token if the parameter defined by
 57  
  * {@link #setParameter(String)} is present.
 58  
  * </p>
 59  
  * 
 60  
  * <p>
 61  
  * An {@link org.acegisecurity.userdetails.UserDetailsService} is required by
 62  
  * this implementation, so that it can construct a valid
 63  
  * <code>Authentication</code> from the returned {@link
 64  
  * org.acegisecurity.userdetails.UserDetails}. This is also necessary so that
 65  
  * the user's password is available and can be checked as part of the encoded
 66  
  * cookie.
 67  
  * </p>
 68  
  * 
 69  
  * <p>
 70  
  * The cookie encoded by this implementation adopts the following form:
 71  
  * 
 72  
  * <pre>
 73  
  * username + &quot;:&quot; + expiryTime + &quot;:&quot; + Md5Hex(username + &quot;:&quot; + expiryTime + &quot;:&quot; + password + &quot;:&quot; + key)
 74  
  * </pre>
 75  
  * 
 76  
  * </p>
 77  
  * <p>
 78  
  * As such, if the user changes their password any remember-me token will be
 79  
  * invalidated. Equally, the system administrator may invalidate every
 80  
  * remember-me token on issue by changing the key. This provides some reasonable
 81  
  * approaches to recovering from a remember-me token being left on a public
 82  
  * machine (eg kiosk system, Internet cafe etc). Most importantly, at no time is
 83  
  * the user's password ever sent to the user agent, providing an important
 84  
  * security safeguard. Unfortunately the username is necessary in this
 85  
  * implementation (as we do not want to rely on a database for remember-me
 86  
  * services) and as such high security applications should be aware of this
 87  
  * occasionally undesired disclosure of a valid username.
 88  
  * </p>
 89  
  * <p>
 90  
  * This is a basic remember-me implementation which is suitable for many
 91  
  * applications. However, we recommend a database-based implementation if you
 92  
  * require a more secure remember-me approach.
 93  
  * </p>
 94  
  * <p>
 95  
  * By default the tokens will be valid for 14 days from the last successful
 96  
  * authentication attempt. This can be changed using
 97  
  * {@link #setTokenValiditySeconds(long)}.
 98  
  * </p>
 99  
  * 
 100  
  * @author Ben Alex
 101  
  * @version $Id: TokenBasedRememberMeServices.java 1871 2007-05-25 03:12:49Z
 102  
  * benalex $
 103  
  */
 104  15
 public class TokenBasedRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
 105  
         // ~ Static fields/initializers
 106  
         // =====================================================================================
 107  
 
 108  
         public static final String ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY = "ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE";
 109  
 
 110  
         public static final String DEFAULT_PARAMETER = "_acegi_security_remember_me";
 111  
 
 112  2
         protected static final Log logger = LogFactory.getLog(TokenBasedRememberMeServices.class);
 113  
 
 114  
         // ~ Instance fields
 115  
         // ================================================================================================
 116  
 
 117  15
         protected AuthenticationDetailsSource authenticationDetailsSource = new AuthenticationDetailsSourceImpl();
 118  
 
 119  
         private String key;
 120  
 
 121  15
         private String parameter = DEFAULT_PARAMETER;
 122  
 
 123  
         private UserDetailsService userDetailsService;
 124  
 
 125  15
         protected long tokenValiditySeconds = 1209600; // 14 days
 126  
 
 127  15
         private boolean alwaysRemember = false;
 128  
 
 129  
         private static final int DEFAULT_ORDER = Integer.MAX_VALUE; // ~ default
 130  
 
 131  15
         private String cookieName = ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY;
 132  
 
 133  
         // ~ Methods
 134  
         // ========================================================================================================
 135  
 
 136  
         public void afterPropertiesSet() throws Exception {
 137  0
                 Assert.hasLength(key);
 138  0
                 Assert.hasLength(parameter);
 139  0
                 Assert.hasLength(cookieName);
 140  0
                 Assert.notNull(userDetailsService);
 141  0
         }
 142  
 
 143  
         /**
 144  
          * Introspects the <code>Applicationcontext</code> for the single instance
 145  
          * of {@link AccessDeniedHandler}. If found invoke
 146  
          * setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) method by
 147  
          * providing the found instance of accessDeniedHandler as a method
 148  
          * parameter. If more than one instance of <code>AccessDeniedHandler</code>
 149  
          * is found, the method throws <code>IllegalStateException</code>.
 150  
          * 
 151  
          * @param applicationContext to locate the instance
 152  
          */
 153  
         private void autoDetectAndUseAnyUserDetailsService(ApplicationContext applicationContext) {
 154  0
                 Map map = applicationContext.getBeansOfType(UserDetailsService.class);
 155  0
                 if (map.size() > 1) {
 156  0
                         throw new IllegalArgumentException(
 157  
                                         "More than one UserDetailsService beans detected please refer to the one using "
 158  
                                                         + " [ principalRepositoryBeanRef  ] " + "attribute");
 159  
                 }
 160  0
                 else if (map.size() == 1) {
 161  0
                         setUserDetailsService((UserDetailsService) map.values().iterator().next());
 162  
                 }
 163  0
         }
 164  
 
 165  
         public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
 166  9
                 Cookie[] cookies = request.getCookies();
 167  
 
 168  9
                 if ((cookies == null) || (cookies.length == 0)) {
 169  1
                         return null;
 170  
                 }
 171  
 
 172  9
                 for (int i = 0; i < cookies.length; i++) {
 173  8
                         if (cookieName.equals(cookies[i].getName())) {
 174  7
                                 String cookieValue = cookies[i].getValue();
 175  
 
 176  8
                                 for (int j = 0; j < cookieValue.length() % 4; j++) {
 177  1
                                         cookieValue = cookieValue + "=";
 178  
                                 }
 179  
 
 180  7
                                 if (Base64.isArrayByteBase64(cookieValue.getBytes())) {
 181  6
                                         if (logger.isDebugEnabled()) {
 182  0
                                                 logger.debug("Remember-me cookie detected");
 183  
                                         }
 184  
 
 185  
                                         // Decode token from Base64
 186  
                                         // format of token is:
 187  
                                         // username + ":" + expiryTime + ":" +
 188  
                                         // Md5Hex(username + ":" + expiryTime + ":" + password + ":"
 189  
                                         // + key)
 190  6
                                         String cookieAsPlainText = new String(Base64.decodeBase64(cookieValue.getBytes()));
 191  6
                                         String[] cookieTokens = StringUtils.delimitedListToStringArray(cookieAsPlainText, ":");
 192  
 
 193  6
                                         if (cookieTokens.length == 3) {
 194  
 
 195  
                                                 long tokenExpiryTime;
 196  
 
 197  
                                                 try {
 198  5
                                                         tokenExpiryTime = new Long(cookieTokens[1]).longValue();
 199  
                                                 }
 200  1
                                                 catch (NumberFormatException nfe) {
 201  1
                                                         cancelCookie(request, response,
 202  
                                                                         "Cookie token[1] did not contain a valid number (contained '" + cookieTokens[1]
 203  
                                                                                         + "')");
 204  
 
 205  1
                                                         return null;
 206  4
                                                 }
 207  
 
 208  4
                                                 if (isTokenExpired(tokenExpiryTime)) {
 209  1
                                                         cancelCookie(request, response, "Cookie token[1] has expired (expired on '"
 210  
                                                                         + new Date(tokenExpiryTime) + "'; current time is '" + new Date() + "')");
 211  
 
 212  1
                                                         return null;
 213  
                                                 }
 214  
 
 215  
                                                 // Check the user exists
 216  
                                                 // Defer lookup until after expiry time checked, to
 217  
                                                 // possibly avoid expensive lookup
 218  3
                                                 UserDetails userDetails = loadUserDetails(request, response, cookieTokens);
 219  
 
 220  3
                                                 if (userDetails == null) {
 221  1
                                                         cancelCookie(request, response, "Cookie token[0] contained username '" + cookieTokens[0]
 222  
                                                                         + "' but was not found");
 223  1
                                                         return null;
 224  
                                                 }
 225  
 
 226  2
                                                 if (!isValidUserDetails(request, response, userDetails, cookieTokens)) {
 227  0
                                                         return null;
 228  
                                                 }
 229  
 
 230  
                                                 // Check signature of token matches remaining details
 231  
                                                 // Must do this after user lookup, as we need the
 232  
                                                 // DAO-derived password
 233  
                                                 // If efficiency was a major issue, just add in a
 234  
                                                 // UserCache implementation,
 235  
                                                 // but recall this method is usually only called one per
 236  
                                                 // HttpSession
 237  
                                                 // (as if the token is valid, it will cause
 238  
                                                 // SecurityContextHolder population, whilst
 239  
                                                 // if invalid, will cause the cookie to be cancelled)
 240  2
                                                 String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails);
 241  
 
 242  2
                                                 if (!expectedTokenSignature.equals(cookieTokens[2])) {
 243  1
                                                         cancelCookie(request, response, "Cookie token[2] contained signature '" + cookieTokens[2]
 244  
                                                                         + "' but expected '" + expectedTokenSignature + "'");
 245  
 
 246  1
                                                         return null;
 247  
                                                 }
 248  
 
 249  
                                                 // By this stage we have a valid token
 250  1
                                                 if (logger.isDebugEnabled()) {
 251  0
                                                         logger.debug("Remember-me cookie accepted");
 252  
                                                 }
 253  
 
 254  1
                                                 RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(this.key, userDetails,
 255  
                                                                 userDetails.getAuthorities());
 256  1
                                                 auth.setDetails(authenticationDetailsSource.buildDetails((HttpServletRequest) request));
 257  
 
 258  1
                                                 return auth;
 259  
                                         }
 260  
                                         else {
 261  1
                                                 cancelCookie(request, response, "Cookie token did not contain 3 tokens; decoded value was '"
 262  
                                                                 + cookieAsPlainText + "'");
 263  
 
 264  1
                                                 return null;
 265  
                                         }
 266  
                                 }
 267  
                                 else {
 268  1
                                         cancelCookie(request, response, "Cookie token was not Base64 encoded; value was '" + cookieValue
 269  
                                                         + "'");
 270  
 
 271  1
                                         return null;
 272  
                                 }
 273  
                         }
 274  
                 }
 275  
 
 276  1
                 return null;
 277  
         }
 278  
 
 279  
         /**
 280  
          * @param tokenExpiryTime
 281  
          * @param userDetails
 282  
          * @return
 283  
          */
 284  
         protected String makeTokenSignature(long tokenExpiryTime, UserDetails userDetails) {
 285  2
                 String expectedTokenSignature = DigestUtils.md5Hex(userDetails.getUsername() + ":" + tokenExpiryTime + ":"
 286  
                                 + userDetails.getPassword() + ":" + this.key);
 287  2
                 return expectedTokenSignature;
 288  
         }
 289  
 
 290  
         protected boolean isValidUserDetails(HttpServletRequest request, HttpServletResponse response,
 291  
                         UserDetails userDetails, String[] cookieTokens) {
 292  
                 // Immediately reject if the user is not allowed to
 293  
                 // login
 294  2
                 if (!userDetails.isAccountNonExpired() || !userDetails.isCredentialsNonExpired() || !userDetails.isEnabled()) {
 295  0
                         cancelCookie(request, response, "Cookie token[0] contained username '" + cookieTokens[0]
 296  
                                         + "' but account has expired, credentials have expired, or user is disabled");
 297  
 
 298  0
                         return false;
 299  
                 }
 300  2
                 return true;
 301  
         }
 302  
 
 303  
         protected UserDetails loadUserDetails(HttpServletRequest request, HttpServletResponse response,
 304  
                         String[] cookieTokens) {
 305  3
                 UserDetails userDetails = null;
 306  
 
 307  
                 try {
 308  3
                         userDetails = this.userDetailsService.loadUserByUsername(cookieTokens[0]);
 309  
                 }
 310  1
                 catch (UsernameNotFoundException notFound) {
 311  1
                         cancelCookie(request, response, "Cookie token[0] contained username '" + cookieTokens[0]
 312  
                                         + "' but was not found");
 313  
 
 314  1
                         return null;
 315  2
                 }
 316  2
                 return userDetails;
 317  
         }
 318  
 
 319  
         protected boolean isTokenExpired(long tokenExpiryTime) {
 320  
                 // Check it has not expired
 321  4
                 if (tokenExpiryTime < System.currentTimeMillis()) {
 322  1
                         return true;
 323  
                 }
 324  3
                 return false;
 325  
         }
 326  
 
 327  
         protected void cancelCookie(HttpServletRequest request, HttpServletResponse response, String reasonForLog) {
 328  8
                 if ((reasonForLog != null) && logger.isDebugEnabled()) {
 329  0
                         logger.debug("Cancelling cookie for reason: " + reasonForLog);
 330  
                 }
 331  
 
 332  8
                 response.addCookie(makeCancelCookie(request));
 333  8
         }
 334  
 
 335  
         public String getKey() {
 336  1
                 return key;
 337  
         }
 338  
 
 339  
         public String getParameter() {
 340  2
                 return parameter;
 341  
         }
 342  
 
 343  
         public long getTokenValiditySeconds() {
 344  3
                 return tokenValiditySeconds;
 345  
         }
 346  
 
 347  
         public UserDetailsService getUserDetailsService() {
 348  1
                 return userDetailsService;
 349  
         }
 350  
 
 351  
         public void loginFail(HttpServletRequest request, HttpServletResponse response) {
 352  1
                 cancelCookie(request, response, "Interactive authentication attempt was unsuccessful");
 353  1
         }
 354  
 
 355  
         protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
 356  3
                 if (alwaysRemember) {
 357  0
                         return true;
 358  
                 }
 359  
 
 360  3
                 return RequestUtils.getBooleanParameter(request, parameter, false);
 361  
         }
 362  
 
 363  
         public void loginSuccess(HttpServletRequest request, HttpServletResponse response,
 364  
                         Authentication successfulAuthentication) {
 365  
                 // Exit if the principal hasn't asked to be remembered
 366  3
                 if (!rememberMeRequested(request, parameter)) {
 367  1
                         if (logger.isDebugEnabled()) {
 368  0
                                 logger.debug("Did not send remember-me cookie (principal did not set parameter '" + this.parameter
 369  
                                                 + "')");
 370  
                         }
 371  
 
 372  1
                         return;
 373  
                 }
 374  
 
 375  
                 // Determine username and password, ensuring empty strings
 376  2
                 Assert.notNull(successfulAuthentication.getPrincipal());
 377  2
                 Assert.notNull(successfulAuthentication.getCredentials());
 378  
 
 379  2
                 String username = retrieveUserName(successfulAuthentication);
 380  2
                 String password = retrievePassword(successfulAuthentication);
 381  
 
 382  
                 // If unable to find a username and password, just abort as
 383  
                 // TokenBasedRememberMeServices unable to construct a valid token in
 384  
                 // this case
 385  2
                 if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
 386  0
                         return;
 387  
                 }
 388  
 
 389  2
                 long expiryTime = System.currentTimeMillis() + (tokenValiditySeconds * 1000);
 390  
 
 391  
                 // construct token to put in cookie; format is:
 392  
                 // username + ":" + expiryTime + ":" + Md5Hex(username + ":" +
 393  
                 // expiryTime + ":" + password + ":" + key)
 394  2
                 String signatureValue = DigestUtils.md5Hex(username + ":" + expiryTime + ":" + password + ":" + key);
 395  2
                 String tokenValue = username + ":" + expiryTime + ":" + signatureValue;
 396  2
                 String tokenValueBase64 = new String(Base64.encodeBase64(tokenValue.getBytes()));
 397  2
                 response.addCookie(makeValidCookie(tokenValueBase64, request, tokenValiditySeconds));
 398  
 
 399  2
                 if (logger.isDebugEnabled()) {
 400  0
                         logger
 401  
                                         .debug("Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime)
 402  
                                                         + "'");
 403  
                 }
 404  2
         }
 405  
 
 406  
         public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
 407  0
                 cancelCookie(request, response, "Logout of user "
 408  
                                 + (authentication == null ? "Unknown" : authentication.getName()));
 409  0
         }
 410  
 
 411  
         protected String retrieveUserName(Authentication successfulAuthentication) {
 412  2
                 if (isInstanceOfUserDetails(successfulAuthentication)) {
 413  1
                         return ((UserDetails) successfulAuthentication.getPrincipal()).getUsername();
 414  
                 }
 415  
                 else {
 416  1
                         return successfulAuthentication.getPrincipal().toString();
 417  
                 }
 418  
         }
 419  
 
 420  
         protected String retrievePassword(Authentication successfulAuthentication) {
 421  2
                 if (isInstanceOfUserDetails(successfulAuthentication)) {
 422  1
                         return ((UserDetails) successfulAuthentication.getPrincipal()).getPassword();
 423  
                 }
 424  
                 else {
 425  1
                         return successfulAuthentication.getCredentials().toString();
 426  
                 }
 427  
         }
 428  
 
 429  
         private boolean isInstanceOfUserDetails(Authentication authentication) {
 430  4
                 return authentication.getPrincipal() instanceof UserDetails;
 431  
         }
 432  
 
 433  
         protected Cookie makeCancelCookie(HttpServletRequest request) {
 434  8
                 Cookie cookie = new Cookie(cookieName, null);
 435  8
                 cookie.setMaxAge(0);
 436  8
                 cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
 437  
 
 438  8
                 return cookie;
 439  
         }
 440  
 
 441  
         protected Cookie makeValidCookie(String tokenValueBase64, HttpServletRequest request, long maxAge) {
 442  2
                 Cookie cookie = new Cookie(cookieName, tokenValueBase64);
 443  2
                 cookie.setMaxAge(new Long(maxAge).intValue());
 444  2
                 cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
 445  
 
 446  2
                 return cookie;
 447  
         }
 448  
 
 449  
         public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
 450  0
                 Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
 451  0
                 this.authenticationDetailsSource = authenticationDetailsSource;
 452  0
         }
 453  
 
 454  
         public void setKey(String key) {
 455  10
                 this.key = key;
 456  10
         }
 457  
 
 458  
         public void setParameter(String parameter) {
 459  1
                 this.parameter = parameter;
 460  1
         }
 461  
 
 462  
         public void setCookieName(String cookieName) {
 463  0
                 this.cookieName = cookieName;
 464  0
         }
 465  
 
 466  
         public void setTokenValiditySeconds(long tokenValiditySeconds) {
 467  1
                 this.tokenValiditySeconds = tokenValiditySeconds;
 468  1
         }
 469  
 
 470  
         public void setUserDetailsService(UserDetailsService userDetailsService) {
 471  10
                 this.userDetailsService = userDetailsService;
 472  10
         }
 473  
 
 474  
         public boolean isAlwaysRemember() {
 475  0
                 return alwaysRemember;
 476  
         }
 477  
 
 478  
         public void setAlwaysRemember(boolean alwaysRemember) {
 479  0
                 this.alwaysRemember = alwaysRemember;
 480  0
         }
 481  
 
 482  
         public String getCookieName() {
 483  0
                 return cookieName;
 484  
         }
 485  
 
 486  
 }