1 /*
2  * JBoss, Home of Professional Open Source.
3  * Copyright 2014 Red Hat, Inc., and individual contributors
4  * as indicated by the @author tags.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * 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, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  */

18 package io.undertow.security.impl;
19
20 import static io.undertow.UndertowLogger.REQUEST_LOGGER;
21 import static io.undertow.UndertowMessages.MESSAGES;
22 import static io.undertow.security.impl.DigestAuthorizationToken.parseHeader;
23 import static io.undertow.util.Headers.AUTHENTICATION_INFO;
24 import static io.undertow.util.Headers.AUTHORIZATION;
25 import static io.undertow.util.Headers.DIGEST;
26 import static io.undertow.util.Headers.NEXT_NONCE;
27 import static io.undertow.util.Headers.WWW_AUTHENTICATE;
28 import static io.undertow.util.StatusCodes.UNAUTHORIZED;
29
30 import io.undertow.UndertowLogger;
31 import io.undertow.security.api.AuthenticationMechanism;
32 import io.undertow.security.api.AuthenticationMechanismFactory;
33 import io.undertow.security.api.NonceManager;
34 import io.undertow.security.api.SecurityContext;
35 import io.undertow.security.idm.Account;
36 import io.undertow.security.idm.DigestAlgorithm;
37 import io.undertow.security.idm.DigestCredential;
38 import io.undertow.security.idm.IdentityManager;
39 import io.undertow.server.HttpServerExchange;
40 import io.undertow.server.handlers.form.FormParserFactory;
41 import io.undertow.util.AttachmentKey;
42 import io.undertow.util.HeaderMap;
43 import io.undertow.util.Headers;
44 import io.undertow.util.HexConverter;
45 import io.undertow.util.StatusCodes;
46
47 import java.nio.charset.StandardCharsets;
48 import java.security.MessageDigest;
49 import java.security.NoSuchAlgorithmException;
50 import java.util.Collections;
51 import java.util.EnumSet;
52 import java.util.Iterator;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Set;
56
57 /**
58  * {@link io.undertow.server.HttpHandler} to handle HTTP Digest authentication, both according to RFC-2617 and draft update to allow additional
59  * algorithms to be used.
60  *
61  * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a>
62  */

63 public class DigestAuthenticationMechanism implements AuthenticationMechanism {
64
65     public static final AuthenticationMechanismFactory FACTORY = new Factory();
66
67     private static final String DEFAULT_NAME = "DIGEST";
68     private static final String DIGEST_PREFIX = DIGEST + " ";
69     private static final int PREFIX_LENGTH = DIGEST_PREFIX.length();
70     private static final String OPAQUE_VALUE = "00000000000000000000000000000000";
71     private static final byte COLON = ':';
72
73     private final String mechanismName;
74     private final IdentityManager identityManager;
75
76     private static final Set<DigestAuthorizationToken> MANDATORY_REQUEST_TOKENS;
77
78     static {
79         Set<DigestAuthorizationToken> mandatoryTokens = EnumSet.noneOf(DigestAuthorizationToken.class);
80         mandatoryTokens.add(DigestAuthorizationToken.USERNAME);
81         mandatoryTokens.add(DigestAuthorizationToken.REALM);
82         mandatoryTokens.add(DigestAuthorizationToken.NONCE);
83         mandatoryTokens.add(DigestAuthorizationToken.DIGEST_URI);
84         mandatoryTokens.add(DigestAuthorizationToken.RESPONSE);
85
86         MANDATORY_REQUEST_TOKENS = Collections.unmodifiableSet(mandatoryTokens);
87     }
88
89     /**
90      * The {@link List} of supported algorithms, this is assumed to be in priority order.
91      */

92     private final List<DigestAlgorithm> supportedAlgorithms;
93     private final List<DigestQop> supportedQops;
94     private final String qopString;
95     private final String realmName; // TODO - Will offer choice once backing store API/SPI is in.
96     private final String domain;
97     private final NonceManager nonceManager;
98
99     // Where do session keys fit? Do we just hang onto a session key or keep visiting the user store to check if the password
100     // has changed?
101     // Maybe even support registration of a session so it can be invalidated?
102     // 2013-05-29 - Session keys will be cached, where a cached key is used the IdentityManager is still given the
103     //              opportunity to check the Account is still valid.
104
105     public DigestAuthenticationMechanism(final List<DigestAlgorithm> supportedAlgorithms, final List<DigestQop> supportedQops,
106             final String realmName, final String domain, final NonceManager nonceManager) {
107         this(supportedAlgorithms, supportedQops, realmName, domain, nonceManager, DEFAULT_NAME);
108     }
109
110     public DigestAuthenticationMechanism(final List<DigestAlgorithm> supportedAlgorithms, final List<DigestQop> supportedQops,
111             final String realmName, final String domain, final NonceManager nonceManager, final String mechanismName) {
112         this(supportedAlgorithms, supportedQops, realmName, domain, nonceManager, mechanismName, null);
113     }
114
115     public DigestAuthenticationMechanism(final List<DigestAlgorithm> supportedAlgorithms, final List<DigestQop> supportedQops,
116             final String realmName, final String domain, final NonceManager nonceManager, final String mechanismName, final IdentityManager identityManager) {
117         this.supportedAlgorithms = supportedAlgorithms;
118         this.supportedQops = supportedQops;
119         this.realmName = realmName;
120         this.domain = domain;
121         this.nonceManager = nonceManager;
122         this.mechanismName = mechanismName;
123         this.identityManager = identityManager;
124
125         if (!supportedQops.isEmpty()) {
126             StringBuilder sb = new StringBuilder();
127             Iterator<DigestQop> it = supportedQops.iterator();
128             sb.append(it.next().getToken());
129             while (it.hasNext()) {
130                 sb.append(",").append(it.next().getToken());
131             }
132             qopString = sb.toString();
133         } else {
134             qopString = null;
135         }
136     }
137
138     public DigestAuthenticationMechanism(final String realmName, final String domain, final String mechanismName) {
139         this(realmName, domain, mechanismName, null);
140     }
141
142     public DigestAuthenticationMechanism(final String realmName, final String domain, final String mechanismName, final IdentityManager identityManager) {
143         this(Collections.singletonList(DigestAlgorithm.MD5), Collections.singletonList(DigestQop.AUTH), realmName, domain, new SimpleNonceManager(), DEFAULT_NAME, identityManager);
144     }
145
146     @SuppressWarnings("deprecation")
147     private IdentityManager getIdentityManager(SecurityContext securityContext) {
148         return identityManager != null ? identityManager : securityContext.getIdentityManager();
149     }
150
151     public AuthenticationMechanismOutcome authenticate(final HttpServerExchange exchange,
152                                                        final SecurityContext securityContext) {
153         List<String> authHeaders = exchange.getRequestHeaders().get(AUTHORIZATION);
154         if (authHeaders != null) {
155             for (String current : authHeaders) {
156                 if (current.startsWith(DIGEST_PREFIX)) {
157                     String digestChallenge = current.substring(PREFIX_LENGTH);
158
159                     try {
160                         DigestContext context = new DigestContext();
161                         Map<DigestAuthorizationToken, String> parsedHeader = parseHeader(digestChallenge);
162                         context.setMethod(exchange.getRequestMethod().toString());
163                         context.setParsedHeader(parsedHeader);
164                         // Some form of Digest authentication is going to occur so get the DigestContext set on the exchange.
165                         exchange.putAttachment(DigestContext.ATTACHMENT_KEY, context);
166
167                         UndertowLogger.SECURITY_LOGGER.debugf("Found digest header %s in %s", current, exchange);
168
169                         return handleDigestHeader(exchange, securityContext);
170                     } catch (Exception e) {
171                         e.printStackTrace();
172                     }
173                 }
174
175                 // By this point we had a header we should have been able to verify but for some reason
176                 // it was not correctly structured.
177                 return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
178             }
179         }
180
181         // No suitable header has been found in this request,
182         return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
183     }
184
185     private AuthenticationMechanismOutcome handleDigestHeader(HttpServerExchange exchange, final SecurityContext securityContext) {
186         DigestContext context = exchange.getAttachment(DigestContext.ATTACHMENT_KEY);
187         Map<DigestAuthorizationToken, String> parsedHeader = context.getParsedHeader();
188         // Step 1 - Verify the set of tokens received to ensure valid values.
189         Set<DigestAuthorizationToken> mandatoryTokens = EnumSet.copyOf(MANDATORY_REQUEST_TOKENS);
190         if (!supportedAlgorithms.contains(DigestAlgorithm.MD5)) {
191             // If we don't support MD5 then the client must choose an algorithm as we can not fall back to MD5.
192             mandatoryTokens.add(DigestAuthorizationToken.ALGORITHM);
193         }
194         if (!supportedQops.isEmpty() && !supportedQops.contains(DigestQop.AUTH)) {
195             // If we do not support auth then we are mandating auth-int so force the client to send a QOP
196             mandatoryTokens.add(DigestAuthorizationToken.MESSAGE_QOP);
197         }
198
199         DigestQop qop = null;
200         // This check is early as is increases the list of mandatory tokens.
201         if (parsedHeader.containsKey(DigestAuthorizationToken.MESSAGE_QOP)) {
202             qop = DigestQop.forName(parsedHeader.get(DigestAuthorizationToken.MESSAGE_QOP));
203             if (qop == null || !supportedQops.contains(qop)) {
204                 // We are also ensuring the client is not trying to force a qop that has been disabled.
205                 REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.MESSAGE_QOP.getName(),
206                         parsedHeader.get(DigestAuthorizationToken.MESSAGE_QOP));
207                 // TODO - This actually needs to result in a HTTP 400 Bad Request response and not a new challenge.
208                 return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
209             }
210             context.setQop(qop);
211             mandatoryTokens.add(DigestAuthorizationToken.CNONCE);
212             mandatoryTokens.add(DigestAuthorizationToken.NONCE_COUNT);
213         }
214
215         // Check all mandatory tokens are present.
216         mandatoryTokens.removeAll(parsedHeader.keySet());
217         if (mandatoryTokens.size() > 0) {
218             for (DigestAuthorizationToken currentToken : mandatoryTokens) {
219                 // TODO - Need a better check and possible concatenate the list of tokens - however
220                 // even having one missing token is not something we should routinely expect.
221                 REQUEST_LOGGER.missingAuthorizationToken(currentToken.getName());
222             }
223             // TODO - This actually needs to result in a HTTP 400 Bad Request response and not a new challenge.
224             return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
225         }
226
227         // Perform some validation of the remaining tokens.
228         if (!realmName.equals(parsedHeader.get(DigestAuthorizationToken.REALM))) {
229             REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.REALM.getName(),
230                     parsedHeader.get(DigestAuthorizationToken.REALM));
231             // TODO - This actually needs to result in a HTTP 400 Bad Request response and not a new challenge.
232             return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
233         }
234
235         if(parsedHeader.containsKey(DigestAuthorizationToken.DIGEST_URI)) {
236             String uri = parsedHeader.get(DigestAuthorizationToken.DIGEST_URI);
237             String requestURI = exchange.getRequestURI();
238             if(!exchange.getQueryString().isEmpty()) {
239                 requestURI = requestURI + "?" + exchange.getQueryString();
240             }
241             if(!uri.equals(requestURI)) {
242                 //it is possible we were given an absolute URI
243                 //we reconstruct the URI from the host header to make sure they match up
244                 //I am not sure if this is overly strict, however I think it is better
245                 //to be safe than sorry
246                 requestURI = exchange.getRequestURL();
247                 if(!exchange.getQueryString().isEmpty()) {
248                     requestURI = requestURI + "?" + exchange.getQueryString();
249                 }
250                 if(!uri.equals(requestURI)) {
251                     //just end the auth process
252                     exchange.setStatusCode(StatusCodes.BAD_REQUEST);
253                     exchange.endExchange();
254                     return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
255                 }
256             }
257         } else {
258             return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
259         }
260
261         if (parsedHeader.containsKey(DigestAuthorizationToken.OPAQUE)) {
262             if (!OPAQUE_VALUE.equals(parsedHeader.get(DigestAuthorizationToken.OPAQUE))) {
263                 REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.OPAQUE.getName(),
264                         parsedHeader.get(DigestAuthorizationToken.OPAQUE));
265                 return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
266             }
267         }
268
269         DigestAlgorithm algorithm;
270         if (parsedHeader.containsKey(DigestAuthorizationToken.ALGORITHM)) {
271             algorithm = DigestAlgorithm.forName(parsedHeader.get(DigestAuthorizationToken.ALGORITHM));
272             if (algorithm == null || !supportedAlgorithms.contains(algorithm)) {
273                 // We are also ensuring the client is not trying to force an algorithm that has been disabled.
274                 REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.ALGORITHM.getName(),
275                         parsedHeader.get(DigestAuthorizationToken.ALGORITHM));
276                 // TODO - This actually needs to result in a HTTP 400 Bad Request response and not a new challenge.
277                 return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
278             }
279         } else {
280             // We know this is safe as the algorithm token was made mandatory
281             // if MD5 is not supported.
282             algorithm = DigestAlgorithm.MD5;
283         }
284
285         try {
286             context.setAlgorithm(algorithm);
287         } catch (NoSuchAlgorithmException e) {
288             /*
289              * This should not be possible in a properly configured installation.
290              */

291             REQUEST_LOGGER.exceptionProcessingRequest(e);
292             return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
293         }
294
295         final String userName = parsedHeader.get(DigestAuthorizationToken.USERNAME);
296         final IdentityManager identityManager = getIdentityManager(securityContext);
297         final Account account;
298
299         if (algorithm.isSession()) {
300             /* This can follow one of the following: -
301              *   1 - New session so use DigestCredentialImpl with the IdentityManager to
302              *       create a new session key.
303              *   2 - Obtain the existing session key from the session store and validate it, just use
304              *       IdentityManager to validate account is still active and the current role assignment.
305              */

306             throw new IllegalStateException("Not yet implemented.");
307         } else {
308             final DigestCredential credential = new DigestCredentialImpl(context);
309             account = identityManager.verify(userName, credential);
310         }
311
312         if (account == null) {
313             // Authentication has failed, this could either be caused by the user not-existing or it
314             // could be caused due to an invalid hash.
315             securityContext.authenticationFailed(MESSAGES.authenticationFailed(userName), mechanismName);
316             return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
317         }
318
319         // Step 3 - Verify that the nonce was eligible to be used.
320         if (!validateNonceUse(context, parsedHeader, exchange)) {
321             // TODO - This is the right place to make use of the decision but the check needs to be much much sooner
322             // otherwise a failure server
323             // side could leave a packet that could be 're-played' after the failed auth.
324             // The username and password verification passed but for some reason we do not like the nonce.
325             context.markStale();
326             // We do not mark as a failure on the security context as this is not quite a failure, a client with a cached nonce
327             // can easily hit this point.
328             return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
329         }
330
331         // We have authenticated the remote user.
332
333         sendAuthenticationInfoHeader(exchange);
334         securityContext.authenticationComplete(account, mechanismName, false);
335         return AuthenticationMechanismOutcome.AUTHENTICATED;
336
337         // Step 4 - Set up any QOP related requirements.
338
339         // TODO - Do QOP
340     }
341
342     private boolean validateRequest(final DigestContext context, final byte[] ha1) {
343         byte[] ha2;
344         DigestQop qop = context.getQop();
345         // Step 2.2 Calculate H(A2)
346         if (qop == null || qop.equals(DigestQop.AUTH)) {
347             ha2 = createHA2Auth(context, context.getParsedHeader());
348         } else {
349             ha2 = createHA2AuthInt();
350         }
351
352         byte[] requestDigest;
353         if (qop == null) {
354             requestDigest = createRFC2069RequestDigest(ha1, ha2, context);
355         } else {
356             requestDigest = createRFC2617RequestDigest(ha1, ha2, context);
357         }
358
359         byte[] providedResponse = context.getParsedHeader().get(DigestAuthorizationToken.RESPONSE).getBytes(StandardCharsets.UTF_8);
360
361         return MessageDigest.isEqual(requestDigest, providedResponse);
362     }
363
364     private boolean validateNonceUse(DigestContext context, Map<DigestAuthorizationToken, String> parsedHeader, final HttpServerExchange exchange) {
365         String suppliedNonce = parsedHeader.get(DigestAuthorizationToken.NONCE);
366         int nonceCount = -1;
367         if (parsedHeader.containsKey(DigestAuthorizationToken.NONCE_COUNT)) {
368             String nonceCountHex = parsedHeader.get(DigestAuthorizationToken.NONCE_COUNT);
369
370             nonceCount = Integer.parseInt(nonceCountHex, 16);
371         }
372
373         context.setNonce(suppliedNonce);
374         // TODO - A replay attempt will need an exception.
375         return (nonceManager.validateNonce(suppliedNonce, nonceCount, exchange));
376     }
377
378     private byte[] createHA2Auth(final DigestContext context, Map<DigestAuthorizationToken, String> parsedHeader) {
379         byte[] method = context.getMethod().getBytes(StandardCharsets.UTF_8);
380         byte[] digestUri = parsedHeader.get(DigestAuthorizationToken.DIGEST_URI).getBytes(StandardCharsets.UTF_8);
381
382         MessageDigest digest = context.getDigest();
383         try {
384             digest.update(method);
385             digest.update(COLON);
386             digest.update(digestUri);
387
388             return HexConverter.convertToHexBytes(digest.digest());
389         } finally {
390             digest.reset();
391         }
392     }
393
394     private byte[] createHA2AuthInt() {
395         // TODO - Implement method.
396         throw new IllegalStateException("Method not implemented.");
397     }
398
399     private byte[] createRFC2069RequestDigest(final byte[] ha1, final byte[] ha2, final DigestContext context) {
400         final MessageDigest digest = context.getDigest();
401         final Map<DigestAuthorizationToken, String> parsedHeader = context.getParsedHeader();
402
403         byte[] nonce = parsedHeader.get(DigestAuthorizationToken.NONCE).getBytes(StandardCharsets.UTF_8);
404
405         try {
406             digest.update(ha1);
407             digest.update(COLON);
408             digest.update(nonce);
409             digest.update(COLON);
410             digest.update(ha2);
411
412             return HexConverter.convertToHexBytes(digest.digest());
413         } finally {
414             digest.reset();
415         }
416     }
417
418     private byte[] createRFC2617RequestDigest(final byte[] ha1, final byte[] ha2, final DigestContext context) {
419         final MessageDigest digest = context.getDigest();
420         final Map<DigestAuthorizationToken, String> parsedHeader = context.getParsedHeader();
421
422         byte[] nonce = parsedHeader.get(DigestAuthorizationToken.NONCE).getBytes(StandardCharsets.UTF_8);
423         byte[] nonceCount = parsedHeader.get(DigestAuthorizationToken.NONCE_COUNT).getBytes(StandardCharsets.UTF_8);
424         byte[] cnonce = parsedHeader.get(DigestAuthorizationToken.CNONCE).getBytes(StandardCharsets.UTF_8);
425         byte[] qop = parsedHeader.get(DigestAuthorizationToken.MESSAGE_QOP).getBytes(StandardCharsets.UTF_8);
426
427         try {
428             digest.update(ha1);
429             digest.update(COLON);
430             digest.update(nonce);
431             digest.update(COLON);
432             digest.update(nonceCount);
433             digest.update(COLON);
434             digest.update(cnonce);
435             digest.update(COLON);
436             digest.update(qop);
437             digest.update(COLON);
438             digest.update(ha2);
439
440             return HexConverter.convertToHexBytes(digest.digest());
441         } finally {
442             digest.reset();
443         }
444     }
445
446     @Override
447     public ChallengeResult sendChallenge(final HttpServerExchange exchange, final SecurityContext securityContext) {
448         DigestContext context = exchange.getAttachment(DigestContext.ATTACHMENT_KEY);
449         boolean stale = context == null ? false : context.isStale();
450
451         StringBuilder rb = new StringBuilder(DIGEST_PREFIX);
452         rb.append(Headers.REALM.toString()).append("=\"").append(realmName).append("\",");
453         rb.append(Headers.DOMAIN.toString()).append("=\"").append(domain).append("\",");
454         // based on security constraints.
455         rb.append(Headers.NONCE.toString()).append("=\"").append(nonceManager.nextNonce(null, exchange)).append("\",");
456         // Not currently using OPAQUE as it offers no integrity, used for session data leaves it vulnerable to
457         // session fixation type issues as well.
458         rb.append(Headers.OPAQUE.toString()).append("=\"00000000000000000000000000000000\"");
459         if (stale) {
460             rb.append(",stale=true");
461         }
462         if (supportedAlgorithms.size() > 0) {
463             // This header will need to be repeated once for each algorithm.
464             rb.append(",").append(Headers.ALGORITHM.toString()).append("=%s");
465         }
466         if (qopString != null) {
467             rb.append(",").append(Headers.QOP.toString()).append("=\"").append(qopString).append("\"");
468         }
469
470         String theChallenge = rb.toString();
471         HeaderMap responseHeader = exchange.getResponseHeaders();
472         if (supportedAlgorithms.isEmpty()) {
473             responseHeader.add(WWW_AUTHENTICATE, theChallenge);
474         } else {
475             for (DigestAlgorithm current : supportedAlgorithms) {
476                 responseHeader.add(WWW_AUTHENTICATE, String.format(theChallenge, current.getToken()));
477             }
478         }
479
480         return new ChallengeResult(true, UNAUTHORIZED);
481     }
482
483     public void sendAuthenticationInfoHeader(final HttpServerExchange exchange) {
484         DigestContext context = exchange.getAttachment(DigestContext.ATTACHMENT_KEY);
485         DigestQop qop = context.getQop();
486         String currentNonce = context.getNonce();
487         String nextNonce = nonceManager.nextNonce(currentNonce, exchange);
488         if (qop != null || !nextNonce.equals(currentNonce)) {
489             StringBuilder sb = new StringBuilder();
490             sb.append(NEXT_NONCE).append("=\"").append(nextNonce).append("\"");
491             if (qop != null) {
492                 Map<DigestAuthorizationToken, String> parsedHeader = context.getParsedHeader();
493                 sb.append(",").append(Headers.QOP.toString()).append("=\"").append(qop.getToken()).append("\"");
494                 byte[] ha1 = context.getHa1();
495                 byte[] ha2;
496
497                 if (qop == DigestQop.AUTH) {
498                     ha2 = createHA2Auth(context);
499                 } else {
500                     ha2 = createHA2AuthInt();
501                 }
502                 String rspauth = new String(createRFC2617RequestDigest(ha1, ha2, context), StandardCharsets.UTF_8);
503                 sb.append(",").append(Headers.RESPONSE_AUTH.toString()).append("=\"").append(rspauth).append("\"");
504                 sb.append(",").append(Headers.CNONCE.toString()).append("=\"").append(parsedHeader.get(DigestAuthorizationToken.CNONCE)).append("\"");
505                 sb.append(",").append(Headers.NONCE_COUNT.toString()).append("=").append(parsedHeader.get(DigestAuthorizationToken.NONCE_COUNT));
506             }
507
508             HeaderMap responseHeader = exchange.getResponseHeaders();
509             responseHeader.add(AUTHENTICATION_INFO, sb.toString());
510         }
511
512         exchange.removeAttachment(DigestContext.ATTACHMENT_KEY);
513     }
514
515     private byte[] createHA2Auth(final DigestContext context) {
516         byte[] digestUri = context.getParsedHeader().get(DigestAuthorizationToken.DIGEST_URI).getBytes(StandardCharsets.UTF_8);
517
518         MessageDigest digest = context.getDigest();
519         try {
520             digest.update(COLON);
521             digest.update(digestUri);
522
523             return HexConverter.convertToHexBytes(digest.digest());
524         } finally {
525             digest.reset();
526         }
527     }
528
529     private static class DigestContext {
530
531         static final AttachmentKey<DigestContext> ATTACHMENT_KEY = AttachmentKey.create(DigestContext.class);
532
533         private String method;
534         private String nonce;
535         private DigestQop qop;
536         private byte[] ha1;
537         private DigestAlgorithm algorithm;
538         private MessageDigest digest;
539         private boolean stale = false;
540         Map<DigestAuthorizationToken, String> parsedHeader;
541
542         String getMethod() {
543             return method;
544         }
545
546         void setMethod(String method) {
547             this.method = method;
548         }
549
550         boolean isStale() {
551             return stale;
552         }
553
554         void markStale() {
555             this.stale = true;
556         }
557
558         String getNonce() {
559             return nonce;
560         }
561
562         void setNonce(String nonce) {
563             this.nonce = nonce;
564         }
565
566         DigestQop getQop() {
567             return qop;
568         }
569
570         void setQop(DigestQop qop) {
571             this.qop = qop;
572         }
573
574         byte[] getHa1() {
575             return ha1;
576         }
577
578         void setHa1(byte[] ha1) {
579             this.ha1 = ha1;
580         }
581
582         DigestAlgorithm getAlgorithm() {
583             return algorithm;
584         }
585
586         void setAlgorithm(DigestAlgorithm algorithm) throws NoSuchAlgorithmException {
587             this.algorithm = algorithm;
588             digest = algorithm.getMessageDigest();
589         }
590
591         MessageDigest getDigest()  {
592             return digest;
593         }
594
595         Map<DigestAuthorizationToken, String> getParsedHeader() {
596             return parsedHeader;
597         }
598
599         void setParsedHeader(Map<DigestAuthorizationToken, String> parsedHeader) {
600             this.parsedHeader = parsedHeader;
601         }
602
603     }
604
605     private class DigestCredentialImpl implements DigestCredential {
606
607         private final DigestContext context;
608
609         private DigestCredentialImpl(final DigestContext digestContext) {
610             this.context = digestContext;
611         }
612
613         @Override
614         public DigestAlgorithm getAlgorithm() {
615             return context.getAlgorithm();
616         }
617
618         @Override
619         public boolean verifyHA1(byte[] ha1) {
620             context.setHa1(ha1); // Cache for subsequent use.
621
622             return validateRequest(context, ha1);
623         }
624
625         @Override
626         public String getRealm() {
627             return realmName;
628         }
629
630         @Override
631         public byte[] getSessionData() {
632             if (!context.getAlgorithm().isSession()) {
633                 throw MESSAGES.noSessionData();
634             }
635
636             byte[] nonce = context.getParsedHeader().get(DigestAuthorizationToken.NONCE).getBytes(StandardCharsets.UTF_8);
637             byte[] cnonce = context.getParsedHeader().get(DigestAuthorizationToken.CNONCE).getBytes(StandardCharsets.UTF_8);
638
639             byte[] response = new byte[nonce.length + cnonce.length + 1];
640             System.arraycopy(nonce, 0, response, 0, nonce.length);
641             response[nonce.length] = ':';
642             System.arraycopy(cnonce, 0, response, nonce.length + 1, cnonce.length);
643
644             return response;
645         }
646
647     }
648
649     public static final class Factory implements AuthenticationMechanismFactory {
650
651         @Deprecated
652         public Factory(IdentityManager identityManager) {}
653
654         public Factory() {}
655
656         @Override
657         public AuthenticationMechanism create(String mechanismName,IdentityManager identityManager, FormParserFactory formParserFactory, Map<String, String> properties) {
658             return new DigestAuthenticationMechanism(properties.get(REALM), properties.get(CONTEXT_PATH), mechanismName, identityManager);
659         }
660     }
661
662 }
663