1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.acegisecurity.ui.rememberme;
17
18 import org.acegisecurity.Authentication;
19 import org.acegisecurity.providers.rememberme.RememberMeAuthenticationToken;
20 import org.acegisecurity.userdetails.UserDetailsService;
21 import org.acegisecurity.userdetails.UserDetails;
22 import org.acegisecurity.userdetails.UsernameNotFoundException;
23
24 import org.apache.commons.codec.binary.Base64;
25 import org.apache.commons.codec.digest.DigestUtils;
26 import org.apache.commons.logging.Log;
27 import org.apache.commons.logging.LogFactory;
28
29 import org.springframework.beans.factory.InitializingBean;
30
31 import org.springframework.util.Assert;
32 import org.springframework.util.StringUtils;
33
34 import org.springframework.web.bind.RequestUtils;
35
36 import java.util.Date;
37
38 import javax.servlet.http.Cookie;
39 import javax.servlet.http.HttpServletRequest;
40 import javax.servlet.http.HttpServletResponse;
41
42
43 /***
44 * Identifies previously remembered users by a Base-64 encoded cookie.
45 *
46 * <p>
47 * This implementation does not rely on an external database, so is attractive
48 * for simple applications. The cookie will be valid for a specific period
49 * from the date of the last {@link #loginSuccess(HttpServletRequest,
50 * HttpServletResponse, Authentication)}. As per the interface contract, this
51 * method will only be called when the principal completes a successful
52 * interactive authentication. As such the time period commences from the last
53 * authentication attempt where they furnished credentials - not the time
54 * period they last logged in via remember-me. The implementation will only
55 * send a remember-me token if the parameter defined by {@link
56 * #setParameter(String)} is present.
57 * </p>
58 *
59 * <p>
60 * An {@link org.acegisecurity.userdetails.UserDetailsService} is required
61 * by this implementation, so that it can construct a valid
62 * <code>Authentication</code> from the returned {@link
63 * org.acegisecurity.userdetails.UserDetails}. This is also necessary so that the
64 * user's password is available and can be checked as part of the encoded
65 * cookie.
66 * </p>
67 *
68 * <p>
69 * The cookie encoded by this implementation adopts the following form:
70 * </p>
71 *
72 * <p>
73 * <code> username + ":" + expiryTime + ":" + Md5Hex(username + ":" +
74 * expiryTime + ":" + password + ":" + key) </code>.
75 * </p>
76 *
77 * <p>
78 * As such, if the user changes their password any remember-me token will be
79 * invalidated. Equally, the system administrator may invalidate every
80 * remember-me token on issue by changing the key. This provides some
81 * reasonable approaches to recovering from a remember-me token being left on
82 * a public machine (eg kiosk system, Internet cafe etc). Most importantly, at
83 * no time is the user's password ever sent to the user agent, providing an
84 * important security safeguard. Unfortunately the username is necessary in
85 * this implementation (as we do not want to rely on a database for
86 * remember-me services) and as such high security applications should be
87 * aware of this occasionally undesired disclosure of a valid username.
88 * </p>
89 *
90 * <p>
91 * This is a basic remember-me implementation which is suitable for many
92 * applications. However, we recommend a database-based implementation if you
93 * require a more secure remember-me approach.
94 * </p>
95 *
96 * <p>
97 * By default the tokens will be valid for 14 days from the last successful
98 * authentication attempt. This can be changed using {@link
99 * #setTokenValiditySeconds(int)}.
100 * </p>
101 *
102 * @author Ben Alex
103 * @version $Id: TokenBasedRememberMeServices.java,v 1.7 2005/11/30 00:20:12 benalex Exp $
104 */
105 public class TokenBasedRememberMeServices implements RememberMeServices,
106 InitializingBean {
107
108
109 public static final String ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY = "ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE";
110 public static final String DEFAULT_PARAMETER = "_acegi_security_remember_me";
111 protected static final Log logger = LogFactory.getLog(TokenBasedRememberMeServices.class);
112
113
114
115 private UserDetailsService userDetailsService;
116 private String key;
117 private String parameter = DEFAULT_PARAMETER;
118 private long tokenValiditySeconds = 1209600;
119
120
121
122 public void setUserDetailsService(UserDetailsService authenticationDao) {
123 this.userDetailsService = authenticationDao;
124 }
125
126 public UserDetailsService getUserDetailsService() {
127 return userDetailsService;
128 }
129
130 public void setKey(String key) {
131 this.key = key;
132 }
133
134 public String getKey() {
135 return key;
136 }
137
138 public void setParameter(String parameter) {
139 this.parameter = parameter;
140 }
141
142 public String getParameter() {
143 return parameter;
144 }
145
146 public void setTokenValiditySeconds(long tokenValiditySeconds) {
147 this.tokenValiditySeconds = tokenValiditySeconds;
148 }
149
150 public long getTokenValiditySeconds() {
151 return tokenValiditySeconds;
152 }
153
154 public void afterPropertiesSet() throws Exception {
155 Assert.hasLength(key);
156 Assert.hasLength(parameter);
157 Assert.notNull(userDetailsService);
158 }
159
160 public Authentication autoLogin(HttpServletRequest request,
161 HttpServletResponse response) {
162 Cookie[] cookies = request.getCookies();
163
164 if ((cookies == null) || (cookies.length == 0)) {
165 return null;
166 }
167
168 for (int i = 0; i < cookies.length; i++) {
169 if (ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY.equals(
170 cookies[i].getName())) {
171 String cookieValue = cookies[i].getValue();
172
173 if (Base64.isArrayByteBase64(cookieValue.getBytes())) {
174 if (logger.isDebugEnabled()) {
175 logger.debug("Remember-me cookie detected");
176 }
177
178
179
180
181 String cookieAsPlainText = new String(Base64.decodeBase64(
182 cookieValue.getBytes()));
183 String[] cookieTokens = StringUtils
184 .delimitedListToStringArray(cookieAsPlainText, ":");
185
186 if (cookieTokens.length == 3) {
187 long tokenExpiryTime;
188
189 try {
190 tokenExpiryTime = new Long(cookieTokens[1])
191 .longValue();
192 } catch (NumberFormatException nfe) {
193 cancelCookie(request, response,
194 "Cookie token[1] did not contain a valid number (contained '"
195 + cookieTokens[1] + "')");
196
197 return null;
198 }
199
200
201 if (tokenExpiryTime < System.currentTimeMillis()) {
202 cancelCookie(request, response,
203 "Cookie token[1] has expired (expired on '"
204 + new Date(tokenExpiryTime)
205 + "'; current time is '" + new Date() + "')");
206
207 return null;
208 }
209
210
211
212 UserDetails userDetails;
213
214 try {
215 userDetails = this.userDetailsService
216 .loadUserByUsername(cookieTokens[0]);
217 } catch (UsernameNotFoundException notFound) {
218 cancelCookie(request, response,
219 "Cookie token[0] contained username '"
220 + cookieTokens[0] + "' but was not found");
221
222 return null;
223 }
224
225
226 if (!userDetails.isAccountNonExpired()
227 || !userDetails.isCredentialsNonExpired()
228 || !userDetails.isEnabled()) {
229 cancelCookie(request, response,
230 "Cookie token[0] contained username '"
231 + cookieTokens[0]
232 + "' but account has expired, credentials have expired, or user is disabled");
233
234 return null;
235 }
236
237
238
239
240
241
242
243 String expectedTokenSignature = DigestUtils.md5Hex(userDetails
244 .getUsername() + ":" + tokenExpiryTime + ":"
245 + userDetails.getPassword() + ":" + this.key);
246
247 if (!expectedTokenSignature.equals(cookieTokens[2])) {
248 cancelCookie(request, response,
249 "Cookie token[2] contained signature '"
250 + cookieTokens[2] + "' but expected '"
251 + expectedTokenSignature + "'");
252
253 return null;
254 }
255
256
257 if (logger.isDebugEnabled()) {
258 logger.debug("Remember-me cookie accepted");
259 }
260
261 return new RememberMeAuthenticationToken(this.key,
262 userDetails, userDetails.getAuthorities());
263 } else {
264 cancelCookie(request, response,
265 "Cookie token did not contain 3 tokens; decoded value was '"
266 + cookieAsPlainText + "'");
267
268 return null;
269 }
270 } else {
271 cancelCookie(request, response,
272 "Cookie token was not Base64 encoded; value was '"
273 + cookieValue + "'");
274
275 return null;
276 }
277 }
278 }
279
280 return null;
281 }
282
283 public void loginFail(HttpServletRequest request,
284 HttpServletResponse response) {
285 cancelCookie(request, response,
286 "Interactive authentication attempt was unsuccessful");
287 }
288
289 public void loginSuccess(HttpServletRequest request,
290 HttpServletResponse response, Authentication successfulAuthentication) {
291
292 if (!RequestUtils.getBooleanParameter(request, parameter, false)) {
293 if (logger.isDebugEnabled()) {
294 logger.debug(
295 "Did not send remember-me cookie (principal did not set parameter '"
296 + this.parameter + "')");
297 }
298
299 return;
300 }
301
302
303 Assert.notNull(successfulAuthentication.getPrincipal());
304 Assert.notNull(successfulAuthentication.getCredentials());
305
306 String username;
307 String password;
308
309 if (successfulAuthentication.getPrincipal() instanceof UserDetails) {
310 username = ((UserDetails) successfulAuthentication.getPrincipal())
311 .getUsername();
312 password = ((UserDetails) successfulAuthentication.getPrincipal())
313 .getPassword();
314 } else {
315 username = successfulAuthentication.getPrincipal().toString();
316 password = successfulAuthentication.getCredentials().toString();
317 }
318
319 Assert.hasLength(username);
320 Assert.hasLength(password);
321
322 long expiryTime = System.currentTimeMillis()
323 + (tokenValiditySeconds * 1000);
324
325
326
327 String signatureValue = new String(DigestUtils.md5Hex(username + ":"
328 + expiryTime + ":" + password + ":" + key));
329 String tokenValue = username + ":" + expiryTime + ":" + signatureValue;
330 String tokenValueBase64 = new String(Base64.encodeBase64(
331 tokenValue.getBytes()));
332 response.addCookie(makeValidCookie(expiryTime, tokenValueBase64));
333
334 if (logger.isDebugEnabled()) {
335 logger.debug("Added remember-me cookie for user '" + username
336 + "', expiry: '" + new Date(expiryTime) + "'");
337 }
338 }
339
340 protected Cookie makeCancelCookie() {
341 Cookie cookie = new Cookie(ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY,
342 null);
343 cookie.setMaxAge(0);
344
345 return cookie;
346 }
347
348 protected Cookie makeValidCookie(long expiryTime, String tokenValueBase64) {
349 Cookie cookie = new Cookie(ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY,
350 tokenValueBase64);
351 cookie.setMaxAge(60 * 60 * 24 * 365 * 5);
352
353 return cookie;
354 }
355
356 private void cancelCookie(HttpServletRequest request,
357 HttpServletResponse response, String reasonForLog) {
358 if ((reasonForLog != null) && logger.isDebugEnabled()) {
359 logger.debug("Cancelling cookie for reason: " + reasonForLog);
360 }
361
362 response.addCookie(makeCancelCookie());
363 }
364 }