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 java.util.function.Supplier;
022
023import org.apache.shiro.lang.codec.Base64;
024import org.apache.shiro.mgt.AbstractRememberMeManager;
025import org.apache.shiro.subject.Subject;
026import org.apache.shiro.subject.SubjectContext;
027import org.apache.shiro.web.servlet.Cookie;
028import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
029import org.apache.shiro.web.servlet.SimpleCookie;
030import org.apache.shiro.web.subject.WebSubject;
031import org.apache.shiro.web.subject.WebSubjectContext;
032import org.apache.shiro.web.util.WebUtils;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036import javax.servlet.ServletRequest;
037import javax.servlet.http.HttpServletRequest;
038import javax.servlet.http.HttpServletResponse;
039import static org.apache.shiro.session.mgt.DefaultSessionManager.SECURE_COOKIE_DISABLED;
040
041
042/**
043 * Remembers a Subject's identity by saving the Subject's {@link Subject#getPrincipals() principals} to a {@link Cookie}
044 * for later retrieval.
045 * <p/>
046 * Cookie attributes (path, domain, maxAge, etc.) may be set on this class's default
047 * {@link #getCookie() cookie} attribute, which acts as a template to use to set all properties of outgoing cookies
048 * created by this implementation.
049 * <p/>
050 * The default cookie has the following attribute values set:
051 * <table>
052 * <tr>
053 * <th>Attribute Name</th>
054 * <th>Value</th>
055 * </tr>
056 * <tr><td>{@link Cookie#getName() name}</td>
057 * <td>{@code rememberMe}</td>
058 * </tr>
059 * <tr>
060 * <td>{@link Cookie#getPath() path}</td>
061 * <td>{@code /}</td>
062 * </tr>
063 * <tr>
064 * <td>{@link Cookie#getMaxAge() maxAge}</td>
065 * <td>{@link Cookie#ONE_YEAR Cookie.ONE_YEAR}</td>
066 * </tr>
067 * </table>
068 * <p/>
069 * Note that because this class subclasses the {@link AbstractRememberMeManager} which already provides serialization
070 * and encryption logic, this class utilizes both for added security before setting the cookie value.
071 *
072 * @since 1.0
073 */
074public class CookieRememberMeManager extends AbstractRememberMeManager {
075
076    /**
077     * The default name of the underlying rememberMe cookie which is {@code rememberMe}.
078     */
079    public static final String DEFAULT_REMEMBER_ME_COOKIE_NAME = "rememberMe";
080
081    private static final Logger LOGGER = LoggerFactory.getLogger(CookieRememberMeManager.class);
082
083    private Cookie cookie;
084
085    /**
086     * Constructs a new {@code CookieRememberMeManager} with a default {@code rememberMe} cookie template.
087     */
088    public CookieRememberMeManager() {
089        setCookie(createDefaultCookie());
090    }
091
092    /**
093     * Constructor. Pass keySupplier that supplies encryption key
094     *
095     * @param keySupplier
096     * @since 2.0
097     */
098    public CookieRememberMeManager(Supplier<byte[]> keySupplier) {
099        super(keySupplier);
100        setCookie(createDefaultCookie());
101    }
102
103    /**
104     * Returns the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by
105     * this {@code RememberMeManager}.  Outgoing cookies will match this one except for the
106     * {@link Cookie#getValue() value} attribute, which is necessarily set dynamically at runtime.
107     * <p/>
108     * Please see the class-level JavaDoc for the default cookie's attribute values.
109     *
110     * @return the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by
111     * this {@code RememberMeManager}.
112     */
113    public Cookie getCookie() {
114        return cookie;
115    }
116
117    /**
118     * Sets the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by
119     * this {@code RememberMeManager}.  Outgoing cookies will match this one except for the
120     * {@link Cookie#getValue() value} attribute, which is necessarily set dynamically at runtime.
121     * <p/>
122     * Please see the class-level JavaDoc for the default cookie's attribute values.
123     *
124     * @param cookie the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created
125     *               by this {@code RememberMeManager}.
126     */
127    @SuppressWarnings({"UnusedDeclaration"})
128    public void setCookie(Cookie cookie) {
129        this.cookie = cookie;
130    }
131
132    /**
133     * Base64-encodes the specified serialized byte array and sets that base64-encoded String as the cookie value.
134     * <p/>
135     * The {@code subject} instance is expected to be a {@link WebSubject} instance with an HTTP Request/Response pair
136     * so an HTTP cookie can be set on the outgoing response.  If it is not a {@code WebSubject} or that
137     * {@code WebSubject} does not have an HTTP Request/Response pair, this implementation does nothing.
138     *
139     * @param subject    the Subject for which the identity is being serialized.
140     * @param serialized the serialized bytes to be persisted.
141     */
142    protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
143
144        if (!WebUtils.isHttp(subject)) {
145            if (LOGGER.isDebugEnabled()) {
146                String msg = "Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet "
147                        + "request and response in order to set the rememberMe cookie. Returning immediately and "
148                        + "ignoring rememberMe operation.";
149                LOGGER.debug(msg);
150            }
151            return;
152        }
153
154
155        HttpServletRequest request = WebUtils.getHttpRequest(subject);
156        HttpServletResponse response = WebUtils.getHttpResponse(subject);
157
158        //base 64 encode it and store as a cookie:
159        String base64 = Base64.encodeToString(serialized);
160
161        //the class attribute is really a template for the outgoing cookies
162        Cookie template = getCookie();
163        Cookie cookie = new SimpleCookie(template);
164        cookie.setValue(base64);
165        cookie.saveTo(request, response);
166    }
167
168
169    private boolean isIdentityRemoved(WebSubjectContext subjectContext) {
170        ServletRequest request = subjectContext.resolveServletRequest();
171        if (request != null) {
172            Boolean removed = (Boolean) request.getAttribute(ShiroHttpServletRequest.IDENTITY_REMOVED_KEY);
173            return removed != null && removed;
174        }
175        return false;
176    }
177
178
179    /**
180     * Returns a previously serialized identity byte array or {@code null} if the byte array could not be acquired.
181     * This implementation retrieves an HTTP cookie, Base64-decodes the cookie value, and returns the resulting byte
182     * array.
183     * <p/>
184     * The {@code SubjectContext} instance is expected to be a {@link WebSubjectContext} instance with an HTTP
185     * Request/Response pair so an HTTP cookie can be retrieved from the incoming request.  If it is not a
186     * {@code WebSubjectContext} or that {@code WebSubjectContext} does not have an HTTP Request/Response pair, this
187     * implementation returns {@code null}.
188     *
189     * @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation, that
190     *                       is being used to construct a {@link Subject} instance.  To be used to assist with data
191     *                       lookup.
192     * @return a previously serialized identity byte array or {@code null} if the byte array could not be acquired.
193     */
194    protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
195
196        if (!WebUtils.isHttp(subjectContext)) {
197            if (LOGGER.isDebugEnabled()) {
198                String msg = "SubjectContext argument is not an HTTP-aware instance.  This is required to obtain a "
199                        + "servlet request and response in order to retrieve the rememberMe cookie. Returning "
200                        + "immediately and ignoring rememberMe operation.";
201                LOGGER.debug(msg);
202            }
203            return null;
204        }
205
206        WebSubjectContext wsc = (WebSubjectContext) subjectContext;
207        if (isIdentityRemoved(wsc)) {
208            return null;
209        }
210
211        HttpServletRequest request = WebUtils.getHttpRequest(wsc);
212        HttpServletResponse response = WebUtils.getHttpResponse(wsc);
213
214        String base64 = getCookie().readValue(request, response);
215        // Browsers do not always remove cookies immediately (SHIRO-183)
216        // ignore cookies that are scheduled for removal
217        if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) {
218            return null;
219        }
220
221        if (base64 != null) {
222            base64 = ensurePadding(base64);
223            if (LOGGER.isTraceEnabled()) {
224                LOGGER.trace("Acquired Base64 encoded identity [" + base64 + "]");
225            }
226            byte[] decoded;
227            try {
228                decoded = Base64.decode(base64);
229            } catch (RuntimeException rtEx) {
230                /*
231                 * https://issues.apache.org/jira/browse/SHIRO-766:
232                 * If the base64 string cannot be decoded, just assume there is no valid cookie value.
233                 * */
234                getCookie().removeFrom(request, response);
235                LOGGER.warn("Unable to decode existing base64 encoded entity: [" + base64 + "].", rtEx);
236                return null;
237            }
238
239            if (LOGGER.isTraceEnabled()) {
240                LOGGER.trace("Base64 decoded byte array length: " + decoded.length + " bytes.");
241            }
242            return decoded;
243        } else {
244            //no cookie set - new site visitor?
245            return null;
246        }
247    }
248
249    /**
250     * Sometimes a user agent will send the rememberMe cookie value without padding,
251     * most likely because {@code =} is a separator in the cookie header.
252     * <p/>
253     * Contributed by Luis Arias.  Thanks Luis!
254     *
255     * @param base64 the base64 encoded String that may need to be padded
256     * @return the base64 String padded if necessary.
257     */
258    protected String ensurePadding(String base64) {
259        int length = base64.length();
260        if (length % 4 != 0) {
261            StringBuilder sb = new StringBuilder(base64);
262            while (sb.length() % 4 != 0) {
263                sb.append('=');
264            }
265            base64 = sb.toString();
266        }
267        return base64;
268    }
269
270    /**
271     * Removes the 'rememberMe' cookie from the associated {@link WebSubject}'s request/response pair.
272     * <p/>
273     * The {@code subject} instance is expected to be a {@link WebSubject} instance with an HTTP Request/Response pair.
274     * If it is not a {@code WebSubject} or that {@code WebSubject} does not have an HTTP Request/Response pair, this
275     * implementation does nothing.
276     *
277     * @param subject the subject instance for which identity data should be forgotten from the underlying persistence
278     */
279    protected void forgetIdentity(Subject subject) {
280        if (WebUtils.isHttp(subject)) {
281            HttpServletRequest request = WebUtils.getHttpRequest(subject);
282            HttpServletResponse response = WebUtils.getHttpResponse(subject);
283            forgetIdentity(request, response);
284        }
285    }
286
287    /**
288     * Removes the 'rememberMe' cookie from the associated {@link WebSubjectContext}'s request/response pair.
289     * <p/>
290     * The {@code SubjectContext} instance is expected to be a {@link WebSubjectContext} instance with an HTTP
291     * Request/Response pair.  If it is not a {@code WebSubjectContext} or that {@code WebSubjectContext} does not
292     * have an HTTP Request/Response pair, this implementation does nothing.
293     *
294     * @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation
295     */
296    public void forgetIdentity(SubjectContext subjectContext) {
297        if (WebUtils.isHttp(subjectContext)) {
298            HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);
299            HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);
300            forgetIdentity(request, response);
301        }
302    }
303
304    /**
305     * Removes the rememberMe cookie from the given request/response pair.
306     *
307     * @param request  the incoming HTTP servlet request
308     * @param response the outgoing HTTP servlet response
309     */
310    private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
311        getCookie().removeFrom(request, response);
312    }
313
314    private Cookie createDefaultCookie() {
315        Cookie cookie = new SimpleCookie(DEFAULT_REMEMBER_ME_COOKIE_NAME);
316        cookie.setHttpOnly(true);
317        if (!Boolean.getBoolean(SECURE_COOKIE_DISABLED)) {
318            cookie.setSecure(true);
319        }
320        //One year should be long enough - most sites won't object to requiring a user to log in if they haven't visited
321        //in a year:
322        cookie.setMaxAge(Cookie.ONE_YEAR);
323        return cookie;
324    }
325}