001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    package org.apache.shiro.web.mgt;
020    
021    import org.apache.shiro.codec.Base64;
022    import org.apache.shiro.mgt.AbstractRememberMeManager;
023    import org.apache.shiro.subject.Subject;
024    import org.apache.shiro.subject.SubjectContext;
025    import org.apache.shiro.web.servlet.Cookie;
026    import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
027    import org.apache.shiro.web.servlet.SimpleCookie;
028    import org.apache.shiro.web.subject.WebSubject;
029    import org.apache.shiro.web.subject.WebSubjectContext;
030    import org.apache.shiro.web.util.WebUtils;
031    import org.slf4j.Logger;
032    import org.slf4j.LoggerFactory;
033    
034    import javax.servlet.ServletRequest;
035    import javax.servlet.http.HttpServletRequest;
036    import javax.servlet.http.HttpServletResponse;
037    
038    
039    /**
040     * Remembers a Subject's identity by saving the Subject's {@link Subject#getPrincipals() principals} to a {@link Cookie}
041     * for later retrieval.
042     * <p/>
043     * Cookie attributes (path, domain, maxAge, etc) may be set on this class's default
044     * {@link #getCookie() cookie} attribute, which acts as a template to use to set all properties of outgoing cookies
045     * created by this implementation.
046     * <p/>
047     * The default cookie has the following attribute values set:
048     * <table>
049     * <tr>
050     * <th>Attribute Name</th>
051     * <th>Value</th>
052     * </tr>
053     * <tr><td>{@link Cookie#getName() name}</td>
054     * <td>{@code rememberMe}</td>
055     * </tr>
056     * <tr>
057     * <td>{@link Cookie#getPath() path}</td>
058     * <td>{@code /}</td>
059     * </tr>
060     * <tr>
061     * <td>{@link Cookie#getMaxAge() maxAge}</td>
062     * <td>{@link Cookie#ONE_YEAR Cookie.ONE_YEAR}</td>
063     * </tr>
064     * </table>
065     * <p/>
066     * Note that because this class subclasses the {@link AbstractRememberMeManager} which already provides serialization
067     * and encryption logic, this class utilizes both for added security before setting the cookie value.
068     *
069     * @since 1.0
070     */
071    public class CookieRememberMeManager extends AbstractRememberMeManager {
072    
073        //TODO - complete JavaDoc
074    
075        private static transient final Logger log = LoggerFactory.getLogger(CookieRememberMeManager.class);
076    
077        /**
078         * The default name of the underlying rememberMe cookie which is {@code rememberMe}.
079         */
080        public static final String DEFAULT_REMEMBER_ME_COOKIE_NAME = "rememberMe";
081    
082        private Cookie cookie;
083    
084        /**
085         * Constructs a new {@code CookieRememberMeManager} with a default {@code rememberMe} cookie template.
086         */
087        public CookieRememberMeManager() {
088            Cookie cookie = new SimpleCookie(DEFAULT_REMEMBER_ME_COOKIE_NAME);
089            cookie.setHttpOnly(true);
090            //One year should be long enough - most sites won't object to requiring a user to log in if they haven't visited
091            //in a year:
092            cookie.setMaxAge(Cookie.ONE_YEAR);
093            this.cookie = cookie;
094        }
095    
096        /**
097         * Returns the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by
098         * this {@code RememberMeManager}.  Outgoing cookies will match this one except for the
099         * {@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            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            this.cookie = cookie;
123        }
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            if (!WebUtils.isHttp(subject)) {
138                if (log.isDebugEnabled()) {
139                    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                    log.debug(msg);
143                }
144                return;
145            }
146    
147    
148            HttpServletRequest request = WebUtils.getHttpRequest(subject);
149            HttpServletResponse response = WebUtils.getHttpResponse(subject);
150    
151            //base 64 encode it and store as a cookie:
152            String base64 = Base64.encodeToString(serialized);
153    
154            Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
155            Cookie cookie = new SimpleCookie(template);
156            cookie.setValue(base64);
157            cookie.saveTo(request, response);
158        }
159    
160        private boolean isIdentityRemoved(WebSubjectContext subjectContext) {
161            ServletRequest request = subjectContext.resolveServletRequest();
162            if (request != null) {
163                Boolean removed = (Boolean) request.getAttribute(ShiroHttpServletRequest.IDENTITY_REMOVED_KEY);
164                return removed != null && removed;
165            }
166            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            if (!WebUtils.isHttp(subjectContext)) {
188                if (log.isDebugEnabled()) {
189                    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                    log.debug(msg);
193                }
194                return null;
195            }
196    
197            WebSubjectContext wsc = (WebSubjectContext) subjectContext;
198            if (isIdentityRemoved(wsc)) {
199                return null;
200            }
201    
202            HttpServletRequest request = WebUtils.getHttpRequest(wsc);
203            HttpServletResponse response = WebUtils.getHttpResponse(wsc);
204    
205            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            if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;
209    
210            if (base64 != null) {
211                base64 = ensurePadding(base64);
212                if (log.isTraceEnabled()) {
213                    log.trace("Acquired Base64 encoded identity [" + base64 + "]");
214                }
215                byte[] decoded = Base64.decode(base64);
216                if (log.isTraceEnabled()) {
217                    log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
218                }
219                return decoded;
220            } else {
221                //no cookie set - new site visitor?
222                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            int length = base64.length();
237            if (length % 4 != 0) {
238                StringBuilder sb = new StringBuilder(base64);
239                for (int i = 0; i < length % 4; ++i) {
240                    sb.append('=');
241                }
242                base64 = sb.toString();
243            }
244            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            if (WebUtils.isHttp(subject)) {
258                HttpServletRequest request = WebUtils.getHttpRequest(subject);
259                HttpServletResponse response = WebUtils.getHttpResponse(subject);
260                forgetIdentity(request, response);
261            }
262        }
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            if (WebUtils.isHttp(subjectContext)) {
275                HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);
276                HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);
277                forgetIdentity(request, response);
278            }
279        }
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            getCookie().removeFrom(request, response);
289        }
290    }
291