Clover coverage report - Acegi Security System for Spring - 1.0.0-RC1
Coverage timestamp: Mon Dec 5 2005 09:05:15 EST
file stats: LOC: 364   Methods: 15
NCLOC: 205   Classes: 1
 
 Source file Conditionals Statements Methods TOTAL
TokenBasedRememberMeServices.java 80% 91.6% 100% 89.8%
coverage coverage
 1    /* Copyright 2004, 2005 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 org.acegisecurity.Authentication;
 19    import org.acegisecurity.providers.rememberme.RememberMeAuthenticationToken;
 20    import org.acegisecurity.userdetails.UserDetailsService;
 21    import org.acegisecurity.userdetails.UserDetails;
 22    import org.acegisecurity.userdetails.UsernameNotFoundException;
 23   
 24    import org.apache.commons.codec.binary.Base64;
 25    import org.apache.commons.codec.digest.DigestUtils;
 26    import org.apache.commons.logging.Log;
 27    import org.apache.commons.logging.LogFactory;
 28   
 29    import org.springframework.beans.factory.InitializingBean;
 30   
 31    import org.springframework.util.Assert;
 32    import org.springframework.util.StringUtils;
 33   
 34    import org.springframework.web.bind.RequestUtils;
 35   
 36    import java.util.Date;
 37   
 38    import javax.servlet.http.Cookie;
 39    import javax.servlet.http.HttpServletRequest;
 40    import javax.servlet.http.HttpServletResponse;
 41   
 42   
 43    /**
 44    * Identifies previously remembered users by a Base-64 encoded cookie.
 45    *
 46    * <p>
 47    * This implementation does not rely on an external database, so is attractive
 48    * for simple applications. The cookie will be valid for a specific period
 49    * from the date of the last {@link #loginSuccess(HttpServletRequest,
 50    * HttpServletResponse, Authentication)}. As per the interface contract, this
 51    * method will only be called when the principal completes a successful
 52    * interactive authentication. As such the time period commences from the last
 53    * authentication attempt where they furnished credentials - not the time
 54    * period they last logged in via remember-me. The implementation will only
 55    * send a remember-me token if the parameter defined by {@link
 56    * #setParameter(String)} is present.
 57    * </p>
 58    *
 59    * <p>
 60    * An {@link org.acegisecurity.userdetails.UserDetailsService} is required
 61    * by this implementation, so that it can construct a valid
 62    * <code>Authentication</code> from the returned {@link
 63    * org.acegisecurity.userdetails.UserDetails}. This is also necessary so that the
 64    * user's password is available and can be checked as part of the encoded
 65    * cookie.
 66    * </p>
 67    *
 68    * <p>
 69    * The cookie encoded by this implementation adopts the following form:
 70    * </p>
 71    *
 72    * <p>
 73    * <code> username + ":" + expiryTime + ":" + Md5Hex(username + ":" +
 74    * expiryTime + ":" + password + ":" + key) </code>.
 75    * </p>
 76    *
 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
 81    * reasonable approaches to recovering from a remember-me token being left on
 82    * a public machine (eg kiosk system, Internet cafe etc). Most importantly, at
 83    * no time is the user's password ever sent to the user agent, providing an
 84    * important security safeguard. Unfortunately the username is necessary in
 85    * this implementation (as we do not want to rely on a database for
 86    * remember-me services) and as such high security applications should be
 87    * aware of this occasionally undesired disclosure of a valid username.
 88    * </p>
 89    *
 90    * <p>
 91    * This is a basic remember-me implementation which is suitable for many
 92    * applications. However, we recommend a database-based implementation if you
 93    * require a more secure remember-me approach.
 94    * </p>
 95    *
 96    * <p>
 97    * By default the tokens will be valid for 14 days from the last successful
 98    * authentication attempt. This can be changed using {@link
 99    * #setTokenValiditySeconds(int)}.
 100    * </p>
 101    *
 102    * @author Ben Alex
 103    * @version $Id: TokenBasedRememberMeServices.java,v 1.7 2005/11/30 00:20:12 benalex Exp $
 104    */
 105    public class TokenBasedRememberMeServices implements RememberMeServices,
 106    InitializingBean {
 107    //~ Static fields/initializers =============================================
 108   
 109    public static final String ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY = "ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE";
 110    public static final String DEFAULT_PARAMETER = "_acegi_security_remember_me";
 111    protected static final Log logger = LogFactory.getLog(TokenBasedRememberMeServices.class);
 112   
 113    //~ Instance fields ========================================================
 114   
 115    private UserDetailsService userDetailsService;
 116    private String key;
 117    private String parameter = DEFAULT_PARAMETER;
 118    private long tokenValiditySeconds = 1209600; // 14 days
 119   
 120    //~ Methods ================================================================
 121   
 122  10 public void setUserDetailsService(UserDetailsService authenticationDao) {
 123  10 this.userDetailsService = authenticationDao;
 124    }
 125   
 126  1 public UserDetailsService getUserDetailsService() {
 127  1 return userDetailsService;
 128    }
 129   
 130  10 public void setKey(String key) {
 131  10 this.key = key;
 132    }
 133   
 134  1 public String getKey() {
 135  1 return key;
 136    }
 137   
 138  1 public void setParameter(String parameter) {
 139  1 this.parameter = parameter;
 140    }
 141   
 142  2 public String getParameter() {
 143  2 return parameter;
 144    }
 145   
 146  1 public void setTokenValiditySeconds(long tokenValiditySeconds) {
 147  1 this.tokenValiditySeconds = tokenValiditySeconds;
 148    }
 149   
 150  1 public long getTokenValiditySeconds() {
 151  1 return tokenValiditySeconds;
 152    }
 153   
 154  9 public void afterPropertiesSet() throws Exception {
 155  9 Assert.hasLength(key);
 156  9 Assert.hasLength(parameter);
 157  9 Assert.notNull(userDetailsService);
 158    }
 159   
 160  9 public Authentication autoLogin(HttpServletRequest request,
 161    HttpServletResponse response) {
 162  9 Cookie[] cookies = request.getCookies();
 163   
 164  9 if ((cookies == null) || (cookies.length == 0)) {
 165  1 return null;
 166    }
 167   
 168  8 for (int i = 0; i < cookies.length; i++) {
 169  8 if (ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY.equals(
 170    cookies[i].getName())) {
 171  7 String cookieValue = cookies[i].getValue();
 172   
 173  7 if (Base64.isArrayByteBase64(cookieValue.getBytes())) {
 174  6 if (logger.isDebugEnabled()) {
 175  0 logger.debug("Remember-me cookie detected");
 176    }
 177   
 178    // Decode token from Base64
 179    // format of token is:
 180    // username + ":" + expiryTime + ":" + Md5Hex(username + ":" + expiryTime + ":" + password + ":" + key)
 181  6 String cookieAsPlainText = new String(Base64.decodeBase64(
 182    cookieValue.getBytes()));
 183  6 String[] cookieTokens = StringUtils
 184    .delimitedListToStringArray(cookieAsPlainText, ":");
 185   
 186  6 if (cookieTokens.length == 3) {
 187  5 long tokenExpiryTime;
 188   
 189  5 try {
 190  5 tokenExpiryTime = new Long(cookieTokens[1])
 191    .longValue();
 192    } catch (NumberFormatException nfe) {
 193  1 cancelCookie(request, response,
 194    "Cookie token[1] did not contain a valid number (contained '"
 195    + cookieTokens[1] + "')");
 196   
 197  1 return null;
 198    }
 199   
 200    // Check it has not expired
 201  4 if (tokenExpiryTime < System.currentTimeMillis()) {
 202  1 cancelCookie(request, response,
 203    "Cookie token[1] has expired (expired on '"
 204    + new Date(tokenExpiryTime)
 205    + "'; current time is '" + new Date() + "')");
 206   
 207  1 return null;
 208    }
 209   
 210    // Check the user exists
 211    // Defer lookup until after expiry time checked, to possibly avoid expensive lookup
 212  3 UserDetails userDetails;
 213   
 214  3 try {
 215  3 userDetails = this.userDetailsService
 216    .loadUserByUsername(cookieTokens[0]);
 217    } catch (UsernameNotFoundException notFound) {
 218  1 cancelCookie(request, response,
 219    "Cookie token[0] contained username '"
 220    + cookieTokens[0] + "' but was not found");
 221   
 222  1 return null;
 223    }
 224   
 225    // Immediately reject if the user is not allowed to login
 226  2 if (!userDetails.isAccountNonExpired()
 227    || !userDetails.isCredentialsNonExpired()
 228    || !userDetails.isEnabled()) {
 229  0 cancelCookie(request, response,
 230    "Cookie token[0] contained username '"
 231    + cookieTokens[0]
 232    + "' but account has expired, credentials have expired, or user is disabled");
 233   
 234  0 return null;
 235    }
 236   
 237    // Check signature of token matches remaining details
 238    // Must do this after user lookup, as we need the DAO-derived password
 239    // If efficiency was a major issue, just add in a UserCache implementation,
 240    // but recall this method is usually only called one per HttpSession
 241    // (as if the token is valid, it will cause SecurityContextHolder population, whilst
 242    // if invalid, will cause the cookie to be cancelled)
 243  2 String expectedTokenSignature = DigestUtils.md5Hex(userDetails
 244    .getUsername() + ":" + tokenExpiryTime + ":"
 245    + userDetails.getPassword() + ":" + this.key);
 246   
 247  2 if (!expectedTokenSignature.equals(cookieTokens[2])) {
 248  1 cancelCookie(request, response,
 249    "Cookie token[2] contained signature '"
 250    + cookieTokens[2] + "' but expected '"
 251    + expectedTokenSignature + "'");
 252   
 253  1 return null;
 254    }
 255   
 256    // By this stage we have a valid token
 257  1 if (logger.isDebugEnabled()) {
 258  0 logger.debug("Remember-me cookie accepted");
 259    }
 260   
 261  1 return new RememberMeAuthenticationToken(this.key,
 262    userDetails, userDetails.getAuthorities());
 263    } else {
 264  1 cancelCookie(request, response,
 265    "Cookie token did not contain 3 tokens; decoded value was '"
 266    + cookieAsPlainText + "'");
 267   
 268  1 return null;
 269    }
 270    } else {
 271  1 cancelCookie(request, response,
 272    "Cookie token was not Base64 encoded; value was '"
 273    + cookieValue + "'");
 274   
 275  1 return null;
 276    }
 277    }
 278    }
 279   
 280  1 return null;
 281    }
 282   
 283  1 public void loginFail(HttpServletRequest request,
 284    HttpServletResponse response) {
 285  1 cancelCookie(request, response,
 286    "Interactive authentication attempt was unsuccessful");
 287    }
 288   
 289  3 public void loginSuccess(HttpServletRequest request,
 290    HttpServletResponse response, Authentication successfulAuthentication) {
 291    // Exit if the principal hasn't asked to be remembered
 292  3 if (!RequestUtils.getBooleanParameter(request, parameter, false)) {
 293  1 if (logger.isDebugEnabled()) {
 294  0 logger.debug(
 295    "Did not send remember-me cookie (principal did not set parameter '"
 296    + this.parameter + "')");
 297    }
 298   
 299  1 return;
 300    }
 301   
 302    // Determine username and password, ensuring empty strings
 303  2 Assert.notNull(successfulAuthentication.getPrincipal());
 304  2 Assert.notNull(successfulAuthentication.getCredentials());
 305   
 306  2 String username;
 307  2 String password;
 308   
 309  2 if (successfulAuthentication.getPrincipal() instanceof UserDetails) {
 310  1 username = ((UserDetails) successfulAuthentication.getPrincipal())
 311    .getUsername();
 312  1 password = ((UserDetails) successfulAuthentication.getPrincipal())
 313    .getPassword();
 314    } else {
 315  1 username = successfulAuthentication.getPrincipal().toString();
 316  1 password = successfulAuthentication.getCredentials().toString();
 317    }
 318   
 319  2 Assert.hasLength(username);
 320  2 Assert.hasLength(password);
 321   
 322  2 long expiryTime = System.currentTimeMillis()
 323    + (tokenValiditySeconds * 1000);
 324   
 325    // construct token to put in cookie; format is:
 326    // username + ":" + expiryTime + ":" + Md5Hex(username + ":" + expiryTime + ":" + password + ":" + key)
 327  2 String signatureValue = new String(DigestUtils.md5Hex(username + ":"
 328    + expiryTime + ":" + password + ":" + key));
 329  2 String tokenValue = username + ":" + expiryTime + ":" + signatureValue;
 330  2 String tokenValueBase64 = new String(Base64.encodeBase64(
 331    tokenValue.getBytes()));
 332  2 response.addCookie(makeValidCookie(expiryTime, tokenValueBase64));
 333   
 334  2 if (logger.isDebugEnabled()) {
 335  0 logger.debug("Added remember-me cookie for user '" + username
 336    + "', expiry: '" + new Date(expiryTime) + "'");
 337    }
 338    }
 339   
 340  7 protected Cookie makeCancelCookie() {
 341  7 Cookie cookie = new Cookie(ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY,
 342    null);
 343  7 cookie.setMaxAge(0);
 344   
 345  7 return cookie;
 346    }
 347   
 348  2 protected Cookie makeValidCookie(long expiryTime, String tokenValueBase64) {
 349  2 Cookie cookie = new Cookie(ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY,
 350    tokenValueBase64);
 351  2 cookie.setMaxAge(60 * 60 * 24 * 365 * 5); // 5 years
 352   
 353  2 return cookie;
 354    }
 355   
 356  7 private void cancelCookie(HttpServletRequest request,
 357    HttpServletResponse response, String reasonForLog) {
 358  7 if ((reasonForLog != null) && logger.isDebugEnabled()) {
 359  0 logger.debug("Cancelling cookie for reason: " + reasonForLog);
 360    }
 361   
 362  7 response.addCookie(makeCancelCookie());
 363    }
 364    }