Coverage Report - org.apache.shiro.web.mgt.CookieRememberMeManager
 
Classes in this File Line Coverage Branch Coverage Complexity
CookieRememberMeManager
85%
58/68
52%
18/34
3.2
 
 1  
 /*
 2  
  * Licensed to the Apache Software Foundation (ASF) under one
 3  
  * or more contributor license agreements.  See the NOTICE file
 4  
  * distributed with this work for additional information
 5  
  * regarding copyright ownership.  The ASF licenses this file
 6  
  * to you under the Apache License, Version 2.0 (the
 7  
  * "License"); you may not use this file except in compliance
 8  
  * with the License.  You may obtain a copy of the License at
 9  
  *
 10  
  *     http://www.apache.org/licenses/LICENSE-2.0
 11  
  *
 12  
  * Unless required by applicable law or agreed to in writing,
 13  
  * software distributed under the License is distributed on an
 14  
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15  
  * KIND, either express or implied.  See the License for the
 16  
  * specific language governing permissions and limitations
 17  
  * under the License.
 18  
  */
 19  
 package org.apache.shiro.web.mgt;
 20  
 
 21  
 import org.apache.shiro.codec.Base64;
 22  
 import org.apache.shiro.mgt.AbstractRememberMeManager;
 23  
 import org.apache.shiro.subject.Subject;
 24  
 import org.apache.shiro.subject.SubjectContext;
 25  
 import org.apache.shiro.web.servlet.Cookie;
 26  
 import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
 27  
 import org.apache.shiro.web.servlet.SimpleCookie;
 28  
 import org.apache.shiro.web.subject.WebSubject;
 29  
 import org.apache.shiro.web.subject.WebSubjectContext;
 30  
 import org.apache.shiro.web.util.WebUtils;
 31  
 import org.slf4j.Logger;
 32  
 import org.slf4j.LoggerFactory;
 33  
 
 34  
 import javax.servlet.ServletRequest;
 35  
 import javax.servlet.http.HttpServletRequest;
 36  
 import javax.servlet.http.HttpServletResponse;
 37  
 
 38  
 
 39  
 /**
 40  
  * Remembers a Subject's identity by saving the Subject's {@link Subject#getPrincipals() principals} to a {@link Cookie}
 41  
  * for later retrieval.
 42  
  * <p/>
 43  
  * Cookie attributes (path, domain, maxAge, etc) may be set on this class's default
 44  
  * {@link #getCookie() cookie} attribute, which acts as a template to use to set all properties of outgoing cookies
 45  
  * created by this implementation.
 46  
  * <p/>
 47  
  * The default cookie has the following attribute values set:
 48  
  * <table>
 49  
  * <tr>
 50  
  * <th>Attribute Name</th>
 51  
  * <th>Value</th>
 52  
  * </tr>
 53  
  * <tr><td>{@link Cookie#getName() name}</td>
 54  
  * <td>{@code rememberMe}</td>
 55  
  * </tr>
 56  
  * <tr>
 57  
  * <td>{@link Cookie#getPath() path}</td>
 58  
  * <td>{@code /}</td>
 59  
  * </tr>
 60  
  * <tr>
 61  
  * <td>{@link Cookie#getMaxAge() maxAge}</td>
 62  
  * <td>{@link Cookie#ONE_YEAR Cookie.ONE_YEAR}</td>
 63  
  * </tr>
 64  
  * </table>
 65  
  * <p/>
 66  
  * Note that because this class subclasses the {@link AbstractRememberMeManager} which already provides serialization
 67  
  * and encryption logic, this class utilizes both for added security before setting the cookie value.
 68  
  *
 69  
  * @since 1.0
 70  
  */
 71  
 public class CookieRememberMeManager extends AbstractRememberMeManager {
 72  
 
 73  
     //TODO - complete JavaDoc
 74  
 
 75  1
     private static transient final Logger log = LoggerFactory.getLogger(CookieRememberMeManager.class);
 76  
 
 77  
     /**
 78  
      * The default name of the underlying rememberMe cookie which is {@code rememberMe}.
 79  
      */
 80  
     public static final String DEFAULT_REMEMBER_ME_COOKIE_NAME = "rememberMe";
 81  
 
 82  
     private Cookie cookie;
 83  
 
 84  
     /**
 85  
      * Constructs a new {@code CookieRememberMeManager} with a default {@code rememberMe} cookie template.
 86  
      */
 87  19
     public CookieRememberMeManager() {
 88  19
         Cookie cookie = new SimpleCookie(DEFAULT_REMEMBER_ME_COOKIE_NAME);
 89  19
         cookie.setHttpOnly(true);
 90  
         //One year should be long enough - most sites won't object to requiring a user to log in if they haven't visited
 91  
         //in a year:
 92  19
         cookie.setMaxAge(Cookie.ONE_YEAR);
 93  19
         this.cookie = cookie;
 94  19
     }
 95  
 
 96  
     /**
 97  
      * Returns the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by
 98  
      * this {@code RememberMeManager}.  Outgoing cookies will match this one except for the
 99  
      * {@link Cookie#getValue() value} attribute, which is necessarily set dynamically at runtime.
 100  
      * <p/>
 101  
      * Please see the class-level JavaDoc for the default cookie's attribute values.
 102  
      *
 103  
      * @return the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by
 104  
      *         this {@code RememberMeManager}.
 105  
      */
 106  
     public Cookie getCookie() {
 107  13
         return cookie;
 108  
     }
 109  
 
 110  
     /**
 111  
      * Sets the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by
 112  
      * this {@code RememberMeManager}.  Outgoing cookies will match this one except for the
 113  
      * {@link Cookie#getValue() value} attribute, which is necessarily set dynamically at runtime.
 114  
      * <p/>
 115  
      * Please see the class-level JavaDoc for the default cookie's attribute values.
 116  
      *
 117  
      * @param cookie the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created
 118  
      *               by this {@code RememberMeManager}.
 119  
      */
 120  
     @SuppressWarnings({"UnusedDeclaration"})
 121  
     public void setCookie(Cookie cookie) {
 122  2
         this.cookie = cookie;
 123  2
     }
 124  
 
 125  
     /**
 126  
      * Base64-encodes the specified serialized byte array and sets that base64-encoded String as the cookie value.
 127  
      * <p/>
 128  
      * The {@code subject} instance is expected to be a {@link WebSubject} instance with an HTTP Request/Response pair
 129  
      * so an HTTP cookie can be set on the outgoing response.  If it is not a {@code WebSubject} or that
 130  
      * {@code WebSubject} does not have an HTTP Request/Response pair, this implementation does nothing.
 131  
      *
 132  
      * @param subject    the Subject for which the identity is being serialized.
 133  
      * @param serialized the serialized bytes to be persisted.
 134  
      */
 135  
     protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
 136  
 
 137  1
         if (!WebUtils.isHttp(subject)) {
 138  0
             if (log.isDebugEnabled()) {
 139  0
                 String msg = "Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet " +
 140  
                         "request and response in order to set the rememberMe cookie. Returning immediately and " +
 141  
                         "ignoring rememberMe operation.";
 142  0
                 log.debug(msg);
 143  
             }
 144  0
             return;
 145  
         }
 146  
 
 147  
 
 148  1
         HttpServletRequest request = WebUtils.getHttpRequest(subject);
 149  1
         HttpServletResponse response = WebUtils.getHttpResponse(subject);
 150  
 
 151  
         //base 64 encode it and store as a cookie:
 152  1
         String base64 = Base64.encodeToString(serialized);
 153  
 
 154  1
         Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
 155  1
         Cookie cookie = new SimpleCookie(template);
 156  1
         cookie.setValue(base64);
 157  1
         cookie.saveTo(request, response);
 158  1
     }
 159  
 
 160  
     private boolean isIdentityRemoved(WebSubjectContext subjectContext) {
 161  8
         ServletRequest request = subjectContext.resolveServletRequest();
 162  8
         if (request != null) {
 163  8
             Boolean removed = (Boolean) request.getAttribute(ShiroHttpServletRequest.IDENTITY_REMOVED_KEY);
 164  8
             return removed != null && removed;
 165  
         }
 166  0
         return false;
 167  
     }
 168  
 
 169  
 
 170  
     /**
 171  
      * Returns a previously serialized identity byte array or {@code null} if the byte array could not be acquired.
 172  
      * This implementation retrieves an HTTP cookie, Base64-decodes the cookie value, and returns the resulting byte
 173  
      * array.
 174  
      * <p/>
 175  
      * The {@code SubjectContext} instance is expected to be a {@link WebSubjectContext} instance with an HTTP
 176  
      * Request/Response pair so an HTTP cookie can be retrieved from the incoming request.  If it is not a
 177  
      * {@code WebSubjectContext} or that {@code WebSubjectContext} does not have an HTTP Request/Response pair, this
 178  
      * implementation returns {@code null}.
 179  
      *
 180  
      * @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation, that
 181  
      *                       is being used to construct a {@link Subject} instance.  To be used to assist with data
 182  
      *                       lookup.
 183  
      * @return a previously serialized identity byte array or {@code null} if the byte array could not be acquired.
 184  
      */
 185  
     protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
 186  
 
 187  8
         if (!WebUtils.isHttp(subjectContext)) {
 188  0
             if (log.isDebugEnabled()) {
 189  0
                 String msg = "SubjectContext argument is not an HTTP-aware instance.  This is required to obtain a " +
 190  
                         "servlet request and response in order to retrieve the rememberMe cookie. Returning " +
 191  
                         "immediately and ignoring rememberMe operation.";
 192  0
                 log.debug(msg);
 193  
             }
 194  0
             return null;
 195  
         }
 196  
 
 197  8
         WebSubjectContext wsc = (WebSubjectContext) subjectContext;
 198  8
         if (isIdentityRemoved(wsc)) {
 199  0
             return null;
 200  
         }
 201  
 
 202  8
         HttpServletRequest request = WebUtils.getHttpRequest(wsc);
 203  8
         HttpServletResponse response = WebUtils.getHttpResponse(wsc);
 204  
 
 205  8
         String base64 = getCookie().readValue(request, response);
 206  
         // Browsers do not always remove cookies immediately (SHIRO-183)
 207  
         // ignore cookies that are scheduled for removal
 208  8
         if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;
 209  
 
 210  7
         if (base64 != null) {
 211  2
             base64 = ensurePadding(base64);
 212  2
             if (log.isTraceEnabled()) {
 213  2
                 log.trace("Acquired Base64 encoded identity [" + base64 + "]");
 214  
             }
 215  2
             byte[] decoded = Base64.decode(base64);
 216  2
             if (log.isTraceEnabled()) {
 217  2
                 log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
 218  
             }
 219  2
             return decoded;
 220  
         } else {
 221  
             //no cookie set - new site visitor?
 222  5
             return null;
 223  
         }
 224  
     }
 225  
 
 226  
     /**
 227  
      * Sometimes a user agent will send the rememberMe cookie value without padding,
 228  
      * most likely because {@code =} is a separator in the cookie header.
 229  
      * <p/>
 230  
      * Contributed by Luis Arias.  Thanks Luis!
 231  
      *
 232  
      * @param base64 the base64 encoded String that may need to be padded
 233  
      * @return the base64 String padded if necessary.
 234  
      */
 235  
     private String ensurePadding(String base64) {
 236  2
         int length = base64.length();
 237  2
         if (length % 4 != 0) {
 238  1
             StringBuilder sb = new StringBuilder(base64);
 239  4
             for (int i = 0; i < length % 4; ++i) {
 240  3
                 sb.append('=');
 241  
             }
 242  1
             base64 = sb.toString();
 243  
         }
 244  2
         return base64;
 245  
     }
 246  
 
 247  
     /**
 248  
      * Removes the 'rememberMe' cookie from the associated {@link WebSubject}'s request/response pair.
 249  
      * <p/>
 250  
      * The {@code subject} instance is expected to be a {@link WebSubject} instance with an HTTP Request/Response pair.
 251  
      * If it is not a {@code WebSubject} or that {@code WebSubject} does not have an HTTP Request/Response pair, this
 252  
      * implementation does nothing.
 253  
      *
 254  
      * @param subject the subject instance for which identity data should be forgotten from the underlying persistence
 255  
      */
 256  
     protected void forgetIdentity(Subject subject) {
 257  3
         if (WebUtils.isHttp(subject)) {
 258  3
             HttpServletRequest request = WebUtils.getHttpRequest(subject);
 259  3
             HttpServletResponse response = WebUtils.getHttpResponse(subject);
 260  3
             forgetIdentity(request, response);
 261  
         }
 262  3
     }
 263  
 
 264  
     /**
 265  
      * Removes the 'rememberMe' cookie from the associated {@link WebSubjectContext}'s request/response pair.
 266  
      * <p/>
 267  
      * The {@code SubjectContext} instance is expected to be a {@link WebSubjectContext} instance with an HTTP
 268  
      * Request/Response pair.  If it is not a {@code WebSubjectContext} or that {@code WebSubjectContext} does not
 269  
      * have an HTTP Request/Response pair, this implementation does nothing.
 270  
      *
 271  
      * @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation
 272  
      */
 273  
     public void forgetIdentity(SubjectContext subjectContext) {
 274  1
         if (WebUtils.isHttp(subjectContext)) {
 275  1
             HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);
 276  1
             HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);
 277  1
             forgetIdentity(request, response);
 278  
         }
 279  1
     }
 280  
 
 281  
     /**
 282  
      * Removes the rememberMe cookie from the given request/response pair.
 283  
      *
 284  
      * @param request  the incoming HTTP servlet request
 285  
      * @param response the outgoing HTTP servlet response
 286  
      */
 287  
     private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
 288  4
         getCookie().removeFrom(request, response);
 289  4
     }
 290  
 }
 291