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 */
019package org.apache.shiro.web.mgt;
020
021import org.apache.shiro.codec.Base64;
022import org.apache.shiro.mgt.AbstractRememberMeManager;
023import org.apache.shiro.subject.Subject;
024import org.apache.shiro.subject.SubjectContext;
025import org.apache.shiro.web.servlet.Cookie;
026import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
027import org.apache.shiro.web.servlet.SimpleCookie;
028import org.apache.shiro.web.subject.WebSubject;
029import org.apache.shiro.web.subject.WebSubjectContext;
030import org.apache.shiro.web.util.WebUtils;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import javax.servlet.ServletRequest;
035import javax.servlet.http.HttpServletRequest;
036import 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 */
071public 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