1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.acegisecurity.ui.digestauth;
17
18 import org.acegisecurity.AcegiMessageSource;
19 import org.acegisecurity.AuthenticationException;
20 import org.acegisecurity.AuthenticationServiceException;
21 import org.acegisecurity.BadCredentialsException;
22
23 import org.acegisecurity.context.SecurityContextHolder;
24
25 import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
26 import org.acegisecurity.providers.dao.UserCache;
27 import org.acegisecurity.providers.dao.cache.NullUserCache;
28
29 import org.acegisecurity.ui.AuthenticationDetailsSource;
30 import org.acegisecurity.ui.AuthenticationDetailsSourceImpl;
31
32 import org.acegisecurity.userdetails.UserDetails;
33 import org.acegisecurity.userdetails.UserDetailsService;
34 import org.acegisecurity.userdetails.UsernameNotFoundException;
35
36 import org.acegisecurity.util.StringSplitUtils;
37
38 import org.apache.commons.codec.binary.Base64;
39 import org.apache.commons.codec.digest.DigestUtils;
40 import org.apache.commons.logging.Log;
41 import org.apache.commons.logging.LogFactory;
42
43 import org.springframework.beans.factory.InitializingBean;
44
45 import org.springframework.context.MessageSource;
46 import org.springframework.context.MessageSourceAware;
47 import org.springframework.context.support.MessageSourceAccessor;
48
49 import org.springframework.util.Assert;
50 import org.springframework.util.StringUtils;
51
52 import java.io.IOException;
53
54 import java.util.Map;
55
56 import javax.servlet.Filter;
57 import javax.servlet.FilterChain;
58 import javax.servlet.FilterConfig;
59 import javax.servlet.ServletException;
60 import javax.servlet.ServletRequest;
61 import javax.servlet.ServletResponse;
62 import javax.servlet.http.HttpServletRequest;
63 import javax.servlet.http.HttpServletResponse;
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87 public class DigestProcessingFilter implements Filter, InitializingBean, MessageSourceAware {
88
89
90 private static final Log logger = LogFactory.getLog(DigestProcessingFilter.class);
91
92
93
94 private AuthenticationDetailsSource authenticationDetailsSource = new AuthenticationDetailsSourceImpl();
95 private DigestProcessingFilterEntryPoint authenticationEntryPoint;
96 protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor();
97 private UserCache userCache = new NullUserCache();
98 private UserDetailsService userDetailsService;
99 private boolean passwordAlreadyEncoded = false;
100
101
102
103 public void afterPropertiesSet() throws Exception {
104 Assert.notNull(userDetailsService, "A UserDetailsService is required");
105 Assert.notNull(authenticationEntryPoint, "A DigestProcessingFilterEntryPoint is required");
106 }
107
108 public void destroy() {
109 }
110
111 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
112 throws IOException, ServletException {
113 if (!(request instanceof HttpServletRequest)) {
114 throw new ServletException("Can only process HttpServletRequest");
115 }
116
117 if (!(response instanceof HttpServletResponse)) {
118 throw new ServletException("Can only process HttpServletResponse");
119 }
120
121 HttpServletRequest httpRequest = (HttpServletRequest) request;
122
123 String header = httpRequest.getHeader("Authorization");
124
125 if (logger.isDebugEnabled()) {
126 logger.debug("Authorization header received from user agent: " + header);
127 }
128
129 if ((header != null) && header.startsWith("Digest ")) {
130 String section212response = header.substring(7);
131
132 String[] headerEntries = StringSplitUtils.splitIgnoringQuotes(section212response, ',');
133 Map headerMap = StringSplitUtils.splitEachArrayElementAndCreateMap(headerEntries, "=", "\"");
134
135 String username = (String) headerMap.get("username");
136 String realm = (String) headerMap.get("realm");
137 String nonce = (String) headerMap.get("nonce");
138 String uri = (String) headerMap.get("uri");
139 String responseDigest = (String) headerMap.get("response");
140 String qop = (String) headerMap.get("qop");
141 String nc = (String) headerMap.get("nc");
142 String cnonce = (String) headerMap.get("cnonce");
143
144
145 if ((username == null) || (realm == null) || (nonce == null) || (uri == null) || (response == null)) {
146 if (logger.isDebugEnabled()) {
147 logger.debug("extracted username: '" + username + "'; realm: '" + username + "'; nonce: '"
148 + username + "'; uri: '" + username + "'; response: '" + username + "'");
149 }
150
151 fail(request, response,
152 new BadCredentialsException(messages.getMessage("DigestProcessingFilter.missingMandatory",
153 new Object[]{section212response}, "Missing mandatory digest value; received header {0}")));
154
155 return;
156 }
157
158
159 if ("auth".equals(qop)) {
160 if ((nc == null) || (cnonce == null)) {
161 if (logger.isDebugEnabled()) {
162 logger.debug("extracted nc: '" + nc + "'; cnonce: '" + cnonce + "'");
163 }
164
165 fail(request, response,
166 new BadCredentialsException(messages.getMessage("DigestProcessingFilter.missingAuth",
167 new Object[]{section212response}, "Missing mandatory digest value; received header {0}")));
168
169 return;
170 }
171 }
172
173
174 if (!this.getAuthenticationEntryPoint().getRealmName().equals(realm)) {
175 fail(request, response,
176 new BadCredentialsException(messages.getMessage("DigestProcessingFilter.incorrectRealm",
177 new Object[]{realm, this.getAuthenticationEntryPoint().getRealmName()},
178 "Response realm name '{0}' does not match system realm name of '{1}'")));
179
180 return;
181 }
182
183
184 if (!Base64.isArrayByteBase64(nonce.getBytes())) {
185 fail(request, response,
186 new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceEncoding",
187 new Object[]{nonce}, "Nonce is not encoded in Base64; received nonce {0}")));
188
189 return;
190 }
191
192
193
194
195 String nonceAsPlainText = new String(Base64.decodeBase64(nonce.getBytes()));
196 String[] nonceTokens = StringUtils.delimitedListToStringArray(nonceAsPlainText, ":");
197
198 if (nonceTokens.length != 2) {
199 fail(request, response,
200 new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceNotTwoTokens",
201 new Object[]{nonceAsPlainText}, "Nonce should have yielded two tokens but was {0}")));
202
203 return;
204 }
205
206
207 long nonceExpiryTime;
208
209 try {
210 nonceExpiryTime = new Long(nonceTokens[0]).longValue();
211 } catch (NumberFormatException nfe) {
212 fail(request, response,
213 new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceNotNumeric",
214 new Object[]{nonceAsPlainText},
215 "Nonce token should have yielded a numeric first token, but was {0}")));
216
217 return;
218 }
219
220
221 String expectedNonceSignature = DigestUtils.md5Hex(nonceExpiryTime + ":"
222 + this.getAuthenticationEntryPoint().getKey());
223
224 if (!expectedNonceSignature.equals(nonceTokens[1])) {
225 fail(request, response,
226 new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceCompromised",
227 new Object[]{nonceAsPlainText}, "Nonce token compromised {0}")));
228
229 return;
230 }
231
232
233
234
235 boolean loadedFromDao = false;
236 UserDetails user = userCache.getUserFromCache(username);
237
238 if (user == null) {
239 loadedFromDao = true;
240
241 try {
242 user = userDetailsService.loadUserByUsername(username);
243 } catch (UsernameNotFoundException notFound) {
244 fail(request, response,
245 new BadCredentialsException(messages.getMessage("DigestProcessingFilter.usernameNotFound",
246 new Object[]{username}, "Username {0} not found")));
247
248 return;
249 }
250
251 if (user == null) {
252 throw new AuthenticationServiceException(
253 "AuthenticationDao returned null, which is an interface contract violation");
254 }
255
256 userCache.putUserInCache(user);
257 }
258
259
260 String serverDigestMd5;
261
262
263 serverDigestMd5 = generateDigest(passwordAlreadyEncoded, username, realm, user.getPassword(),
264 ((HttpServletRequest) request).getMethod(), uri, qop, nonce, nc, cnonce);
265
266
267 if (!serverDigestMd5.equals(responseDigest) && !loadedFromDao) {
268 if (logger.isDebugEnabled()) {
269 logger.debug(
270 "Digest comparison failure; trying to refresh user from DAO in case password had changed");
271 }
272
273 try {
274 user = userDetailsService.loadUserByUsername(username);
275 } catch (UsernameNotFoundException notFound) {
276
277 fail(request, response,
278 new BadCredentialsException(messages.getMessage("DigestProcessingFilter.usernameNotFound",
279 new Object[]{username}, "Username {0} not found")));
280 }
281
282 userCache.putUserInCache(user);
283
284
285 serverDigestMd5 = generateDigest(passwordAlreadyEncoded, username, realm, user.getPassword(),
286 ((HttpServletRequest) request).getMethod(), uri, qop, nonce, nc, cnonce);
287 }
288
289
290 if (!serverDigestMd5.equals(responseDigest)) {
291 if (logger.isDebugEnabled()) {
292 logger.debug("Expected response: '" + serverDigestMd5 + "' but received: '" + responseDigest
293 + "'; is AuthenticationDao returning clear text passwords?");
294 }
295
296 fail(request, response,
297 new BadCredentialsException(messages.getMessage("DigestProcessingFilter.incorrectResponse",
298 "Incorrect response")));
299
300 return;
301 }
302
303
304
305
306
307 if (nonceExpiryTime < System.currentTimeMillis()) {
308 fail(request, response,
309 new NonceExpiredException(messages.getMessage("DigestProcessingFilter.nonceExpired",
310 "Nonce has expired/timed out")));
311
312 return;
313 }
314
315 if (logger.isDebugEnabled()) {
316 logger.debug("Authentication success for user: '" + username + "' with response: '" + responseDigest
317 + "'");
318 }
319
320 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user,
321 user.getPassword());
322
323 authRequest.setDetails(authenticationDetailsSource.buildDetails((HttpServletRequest) request));
324
325 SecurityContextHolder.getContext().setAuthentication(authRequest);
326 }
327
328 chain.doFilter(request, response);
329 }
330
331 public static String encodePasswordInA1Format(String username, String realm, String password) {
332 String a1 = username + ":" + realm + ":" + password;
333 String a1Md5 = new String(DigestUtils.md5Hex(a1));
334
335 return a1Md5;
336 }
337
338 private void fail(ServletRequest request, ServletResponse response, AuthenticationException failed)
339 throws IOException, ServletException {
340 SecurityContextHolder.getContext().setAuthentication(null);
341
342 if (logger.isDebugEnabled()) {
343 logger.debug(failed);
344 }
345
346 authenticationEntryPoint.commence(request, response, failed);
347 }
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368 public static String generateDigest(boolean passwordAlreadyEncoded, String username, String realm, String password,
369 String httpMethod, String uri, String qop, String nonce, String nc, String cnonce)
370 throws IllegalArgumentException {
371 String a1Md5 = null;
372 String a2 = httpMethod + ":" + uri;
373 String a2Md5 = new String(DigestUtils.md5Hex(a2));
374
375 if (passwordAlreadyEncoded) {
376 a1Md5 = password;
377 } else {
378 a1Md5 = encodePasswordInA1Format(username, realm, password);
379 }
380
381 String digest;
382
383 if (qop == null) {
384
385 digest = a1Md5 + ":" + nonce + ":" + a2Md5;
386 } else if ("auth".equals(qop)) {
387
388 digest = a1Md5 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + a2Md5;
389 } else {
390 throw new IllegalArgumentException("This method does not support a qop: '" + qop + "'");
391 }
392
393 String digestMd5 = new String(DigestUtils.md5Hex(digest));
394
395 return digestMd5;
396 }
397
398 public DigestProcessingFilterEntryPoint getAuthenticationEntryPoint() {
399 return authenticationEntryPoint;
400 }
401
402 public UserCache getUserCache() {
403 return userCache;
404 }
405
406 public UserDetailsService getUserDetailsService() {
407 return userDetailsService;
408 }
409
410 public void init(FilterConfig ignored) throws ServletException {
411 }
412
413 public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
414 Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
415 this.authenticationDetailsSource = authenticationDetailsSource;
416 }
417
418 public void setAuthenticationEntryPoint(DigestProcessingFilterEntryPoint authenticationEntryPoint) {
419 this.authenticationEntryPoint = authenticationEntryPoint;
420 }
421
422 public void setMessageSource(MessageSource messageSource) {
423 this.messages = new MessageSourceAccessor(messageSource);
424 }
425
426 public void setPasswordAlreadyEncoded(boolean passwordAlreadyEncoded) {
427 this.passwordAlreadyEncoded = passwordAlreadyEncoded;
428 }
429
430 public void setUserCache(UserCache userCache) {
431 this.userCache = userCache;
432 }
433
434 public void setUserDetailsService(UserDetailsService userDetailsService) {
435 this.userDetailsService = userDetailsService;
436 }
437 }