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 java.util.function.Supplier;
22
23 import org.apache.shiro.lang.codec.Base64;
24 import org.apache.shiro.mgt.AbstractRememberMeManager;
25 import org.apache.shiro.subject.Subject;
26 import org.apache.shiro.subject.SubjectContext;
27 import org.apache.shiro.web.servlet.Cookie;
28 import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
29 import org.apache.shiro.web.servlet.SimpleCookie;
30 import org.apache.shiro.web.subject.WebSubject;
31 import org.apache.shiro.web.subject.WebSubjectContext;
32 import org.apache.shiro.web.util.WebUtils;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
36 import javax.servlet.ServletRequest;
37 import javax.servlet.http.HttpServletRequest;
38 import javax.servlet.http.HttpServletResponse;
39
40
41 /**
42 * Remembers a Subject's identity by saving the Subject's {@link Subject#getPrincipals() principals} to a {@link Cookie}
43 * for later retrieval.
44 * <p/>
45 * Cookie attributes (path, domain, maxAge, etc.) may be set on this class's default
46 * {@link #getCookie() cookie} attribute, which acts as a template to use to set all properties of outgoing cookies
47 * created by this implementation.
48 * <p/>
49 * The default cookie has the following attribute values set:
50 * <table>
51 * <tr>
52 * <th>Attribute Name</th>
53 * <th>Value</th>
54 * </tr>
55 * <tr><td>{@link Cookie#getName() name}</td>
56 * <td>{@code rememberMe}</td>
57 * </tr>
58 * <tr>
59 * <td>{@link Cookie#getPath() path}</td>
60 * <td>{@code /}</td>
61 * </tr>
62 * <tr>
63 * <td>{@link Cookie#getMaxAge() maxAge}</td>
64 * <td>{@link Cookie#ONE_YEAR Cookie.ONE_YEAR}</td>
65 * </tr>
66 * </table>
67 * <p/>
68 * Note that because this class subclasses the {@link AbstractRememberMeManager} which already provides serialization
69 * and encryption logic, this class utilizes both for added security before setting the cookie value.
70 *
71 * @since 1.0
72 */
73 public class CookieRememberMeManager extends AbstractRememberMeManager {
74
75 /**
76 * The default name of the underlying rememberMe cookie which is {@code rememberMe}.
77 */
78 public static final String DEFAULT_REMEMBER_ME_COOKIE_NAME = "rememberMe";
79
80 private static final Logger LOGGER = LoggerFactory.getLogger(CookieRememberMeManager.class);
81
82 private Cookie cookie;
83
84 /**
85 * Constructs a new {@code CookieRememberMeManager} with a default {@code rememberMe} cookie template.
86 */
87 public CookieRememberMeManager() {
88 setCookie(createDefaultCookie());
89 }
90
91 /**
92 * Constructor. Pass keySupplier that supplies encryption key
93 *
94 * @param keySupplier
95 * @since 2.0
96 */
97 public CookieRememberMeManager(Supplier<byte[]> keySupplier) {
98 super(keySupplier);
99 setCookie(createDefaultCookie());
100 }
101
102 /**
103 * Returns the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by
104 * this {@code RememberMeManager}. Outgoing cookies will match this one except for the
105 * {@link Cookie#getValue() value} attribute, which is necessarily set dynamically at runtime.
106 * <p/>
107 * Please see the class-level JavaDoc for the default cookie's attribute values.
108 *
109 * @return the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by
110 * this {@code RememberMeManager}.
111 */
112 public Cookie getCookie() {
113 return cookie;
114 }
115
116 /**
117 * Sets the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created by
118 * this {@code RememberMeManager}. Outgoing cookies will match this one except for the
119 * {@link Cookie#getValue() value} attribute, which is necessarily set dynamically at runtime.
120 * <p/>
121 * Please see the class-level JavaDoc for the default cookie's attribute values.
122 *
123 * @param cookie the cookie 'template' that will be used to set all attributes of outgoing rememberMe cookies created
124 * by this {@code RememberMeManager}.
125 */
126 @SuppressWarnings({"UnusedDeclaration"})
127 public void setCookie(Cookie cookie) {
128 this.cookie = cookie;
129 }
130
131 /**
132 * Base64-encodes the specified serialized byte array and sets that base64-encoded String as the cookie value.
133 * <p/>
134 * The {@code subject} instance is expected to be a {@link WebSubject} instance with an HTTP Request/Response pair
135 * so an HTTP cookie can be set on the outgoing response. If it is not a {@code WebSubject} or that
136 * {@code WebSubject} does not have an HTTP Request/Response pair, this implementation does nothing.
137 *
138 * @param subject the Subject for which the identity is being serialized.
139 * @param serialized the serialized bytes to be persisted.
140 */
141 protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
142
143 if (!WebUtils.isHttp(subject)) {
144 if (LOGGER.isDebugEnabled()) {
145 String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet "
146 + "request and response in order to set the rememberMe cookie. Returning immediately and "
147 + "ignoring rememberMe operation.";
148 LOGGER.debug(msg);
149 }
150 return;
151 }
152
153
154 HttpServletRequest request = WebUtils.getHttpRequest(subject);
155 HttpServletResponse response = WebUtils.getHttpResponse(subject);
156
157 //base 64 encode it and store as a cookie:
158 String base64 = Base64.encodeToString(serialized);
159
160 //the class attribute is really a template for the outgoing cookies
161 Cookie template = getCookie();
162 Cookie cookie = new SimpleCookie(template);
163 cookie.setValue(base64);
164 cookie.saveTo(request, response);
165 }
166
167
168 private boolean isIdentityRemoved(WebSubjectContext subjectContext) {
169 ServletRequest request = subjectContext.resolveServletRequest();
170 if (request != null) {
171 Boolean removed = (Boolean) request.getAttribute(ShiroHttpServletRequest.IDENTITY_REMOVED_KEY);
172 return removed != null && removed;
173 }
174 return false;
175 }
176
177
178 /**
179 * Returns a previously serialized identity byte array or {@code null} if the byte array could not be acquired.
180 * This implementation retrieves an HTTP cookie, Base64-decodes the cookie value, and returns the resulting byte
181 * array.
182 * <p/>
183 * The {@code SubjectContext} instance is expected to be a {@link WebSubjectContext} instance with an HTTP
184 * Request/Response pair so an HTTP cookie can be retrieved from the incoming request. If it is not a
185 * {@code WebSubjectContext} or that {@code WebSubjectContext} does not have an HTTP Request/Response pair, this
186 * implementation returns {@code null}.
187 *
188 * @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation, that
189 * is being used to construct a {@link Subject} instance. To be used to assist with data
190 * lookup.
191 * @return a previously serialized identity byte array or {@code null} if the byte array could not be acquired.
192 */
193 protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
194
195 if (!WebUtils.isHttp(subjectContext)) {
196 if (LOGGER.isDebugEnabled()) {
197 String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a "
198 + "servlet request and response in order to retrieve the rememberMe cookie. Returning "
199 + "immediately and ignoring rememberMe operation.";
200 LOGGER.debug(msg);
201 }
202 return null;
203 }
204
205 WebSubjectContext wsc = (WebSubjectContext) subjectContext;
206 if (isIdentityRemoved(wsc)) {
207 return null;
208 }
209
210 HttpServletRequest request = WebUtils.getHttpRequest(wsc);
211 HttpServletResponse response = WebUtils.getHttpResponse(wsc);
212
213 String base64 = getCookie().readValue(request, response);
214 // Browsers do not always remove cookies immediately (SHIRO-183)
215 // ignore cookies that are scheduled for removal
216 if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) {
217 return null;
218 }
219
220 if (base64 != null) {
221 base64 = ensurePadding(base64);
222 if (LOGGER.isTraceEnabled()) {
223 LOGGER.trace("Acquired Base64 encoded identity [" + base64 + "]");
224 }
225 byte[] decoded;
226 try {
227 decoded = Base64.decode(base64);
228 } catch (RuntimeException rtEx) {
229 /*
230 * https://issues.apache.org/jira/browse/SHIRO-766:
231 * If the base64 string cannot be decoded, just assume there is no valid cookie value.
232 * */
233 getCookie().removeFrom(request, response);
234 LOGGER.warn("Unable to decode existing base64 encoded entity: [" + base64 + "].", rtEx);
235 return null;
236 }
237
238 if (LOGGER.isTraceEnabled()) {
239 LOGGER.trace("Base64 decoded byte array length: " + decoded.length + " bytes.");
240 }
241 return decoded;
242 } else {
243 //no cookie set - new site visitor?
244 return null;
245 }
246 }
247
248 /**
249 * Sometimes a user agent will send the rememberMe cookie value without padding,
250 * most likely because {@code =} is a separator in the cookie header.
251 * <p/>
252 * Contributed by Luis Arias. Thanks Luis!
253 *
254 * @param base64 the base64 encoded String that may need to be padded
255 * @return the base64 String padded if necessary.
256 */
257 protected String ensurePadding(String base64) {
258 int length = base64.length();
259 if (length % 4 != 0) {
260 StringBuilder sb = new StringBuilder(base64);
261 while (sb.length() % 4 != 0) {
262 sb.append('=');
263 }
264 base64 = sb.toString();
265 }
266 return base64;
267 }
268
269 /**
270 * Removes the 'rememberMe' cookie from the associated {@link WebSubject}'s request/response pair.
271 * <p/>
272 * The {@code subject} instance is expected to be a {@link WebSubject} instance with an HTTP Request/Response pair.
273 * If it is not a {@code WebSubject} or that {@code WebSubject} does not have an HTTP Request/Response pair, this
274 * implementation does nothing.
275 *
276 * @param subject the subject instance for which identity data should be forgotten from the underlying persistence
277 */
278 protected void forgetIdentity(Subject subject) {
279 if (WebUtils.isHttp(subject)) {
280 HttpServletRequest request = WebUtils.getHttpRequest(subject);
281 HttpServletResponse response = WebUtils.getHttpResponse(subject);
282 forgetIdentity(request, response);
283 }
284 }
285
286 /**
287 * Removes the 'rememberMe' cookie from the associated {@link WebSubjectContext}'s request/response pair.
288 * <p/>
289 * The {@code SubjectContext} instance is expected to be a {@link WebSubjectContext} instance with an HTTP
290 * Request/Response pair. If it is not a {@code WebSubjectContext} or that {@code WebSubjectContext} does not
291 * have an HTTP Request/Response pair, this implementation does nothing.
292 *
293 * @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation
294 */
295 public void forgetIdentity(SubjectContext subjectContext) {
296 if (WebUtils.isHttp(subjectContext)) {
297 HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);
298 HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);
299 forgetIdentity(request, response);
300 }
301 }
302
303 /**
304 * Removes the rememberMe cookie from the given request/response pair.
305 *
306 * @param request the incoming HTTP servlet request
307 * @param response the outgoing HTTP servlet response
308 */
309 private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
310 getCookie().removeFrom(request, response);
311 }
312
313 private Cookie createDefaultCookie() {
314 Cookie cookie = new SimpleCookie(DEFAULT_REMEMBER_ME_COOKIE_NAME);
315 cookie.setHttpOnly(true);
316 //One year should be long enough - most sites won't object to requiring a user to log in if they haven't visited
317 //in a year:
318 cookie.setMaxAge(Cookie.ONE_YEAR);
319 return cookie;
320 }
321 }