-
Notifications
You must be signed in to change notification settings - Fork 974
/
DigestAuthenticationMechanism.java
670 lines (562 loc) · 29.1 KB
/
DigestAuthenticationMechanism.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.undertow.security.impl;
import static io.undertow.UndertowLogger.REQUEST_LOGGER;
import static io.undertow.UndertowMessages.MESSAGES;
import static io.undertow.security.impl.DigestAuthorizationToken.parseHeader;
import static io.undertow.util.Headers.AUTHENTICATION_INFO;
import static io.undertow.util.Headers.AUTHORIZATION;
import static io.undertow.util.Headers.DIGEST;
import static io.undertow.util.Headers.NEXT_NONCE;
import static io.undertow.util.Headers.WWW_AUTHENTICATE;
import static io.undertow.util.StatusCodes.UNAUTHORIZED;
import io.undertow.UndertowLogger;
import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.AuthenticationMechanismFactory;
import io.undertow.security.api.NonceManager;
import io.undertow.security.api.SecurityContext;
import io.undertow.security.idm.Account;
import io.undertow.security.idm.DigestAlgorithm;
import io.undertow.security.idm.DigestCredential;
import io.undertow.security.idm.IdentityManager;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.form.FormParserFactory;
import io.undertow.util.AttachmentKey;
import io.undertow.util.HeaderMap;
import io.undertow.util.Headers;
import io.undertow.util.HexConverter;
import io.undertow.util.Sessions;
import io.undertow.util.StatusCodes;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* {@link io.undertow.server.HttpHandler} to handle HTTP Digest authentication, both according to RFC-2617 and draft update to allow additional
* algorithms to be used.
*
* @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a>
*/
public class DigestAuthenticationMechanism implements AuthenticationMechanism {
public static final AuthenticationMechanismFactory FACTORY = new Factory();
private static final String DEFAULT_NAME = "DIGEST";
private static final String DIGEST_PREFIX = DIGEST + " ";
private static final int PREFIX_LENGTH = DIGEST_PREFIX.length();
private static final String OPAQUE_VALUE = "00000000000000000000000000000000";
private static final byte COLON = ':';
private final String mechanismName;
private final IdentityManager identityManager;
private static final Set<DigestAuthorizationToken> MANDATORY_REQUEST_TOKENS;
static {
Set<DigestAuthorizationToken> mandatoryTokens = EnumSet.noneOf(DigestAuthorizationToken.class);
mandatoryTokens.add(DigestAuthorizationToken.USERNAME);
mandatoryTokens.add(DigestAuthorizationToken.REALM);
mandatoryTokens.add(DigestAuthorizationToken.NONCE);
mandatoryTokens.add(DigestAuthorizationToken.DIGEST_URI);
mandatoryTokens.add(DigestAuthorizationToken.RESPONSE);
MANDATORY_REQUEST_TOKENS = Collections.unmodifiableSet(mandatoryTokens);
}
/**
* The {@link List} of supported algorithms, this is assumed to be in priority order.
*/
private final List<DigestAlgorithm> supportedAlgorithms;
private final List<DigestQop> supportedQops;
private final String qopString;
private final String realmName; // TODO - Will offer choice once backing store API/SPI is in.
private final String domain;
private final NonceManager nonceManager;
// Where do session keys fit? Do we just hang onto a session key or keep visiting the user store to check if the password
// has changed?
// Maybe even support registration of a session so it can be invalidated?
// 2013-05-29 - Session keys will be cached, where a cached key is used the IdentityManager is still given the
// opportunity to check the Account is still valid.
public DigestAuthenticationMechanism(final List<DigestAlgorithm> supportedAlgorithms, final List<DigestQop> supportedQops,
final String realmName, final String domain, final NonceManager nonceManager) {
this(supportedAlgorithms, supportedQops, realmName, domain, nonceManager, DEFAULT_NAME);
}
public DigestAuthenticationMechanism(final List<DigestAlgorithm> supportedAlgorithms, final List<DigestQop> supportedQops,
final String realmName, final String domain, final NonceManager nonceManager, final String mechanismName) {
this(supportedAlgorithms, supportedQops, realmName, domain, nonceManager, mechanismName, null);
}
public DigestAuthenticationMechanism(final List<DigestAlgorithm> supportedAlgorithms, final List<DigestQop> supportedQops,
final String realmName, final String domain, final NonceManager nonceManager, final String mechanismName, final IdentityManager identityManager) {
this.supportedAlgorithms = supportedAlgorithms;
this.supportedQops = supportedQops;
this.realmName = realmName;
this.domain = domain;
this.nonceManager = nonceManager;
this.mechanismName = mechanismName;
this.identityManager = identityManager;
if (!supportedQops.isEmpty()) {
StringBuilder sb = new StringBuilder();
Iterator<DigestQop> it = supportedQops.iterator();
sb.append(it.next().getToken());
while (it.hasNext()) {
sb.append(",").append(it.next().getToken());
}
qopString = sb.toString();
} else {
qopString = null;
}
}
public DigestAuthenticationMechanism(final String realmName, final String domain, final String mechanismName) {
this(realmName, domain, mechanismName, null);
}
public DigestAuthenticationMechanism(final String realmName, final String domain, final String mechanismName, final IdentityManager identityManager) {
this(Collections.singletonList(DigestAlgorithm.MD5), Collections.singletonList(DigestQop.AUTH), realmName, domain, new SimpleNonceManager(), DEFAULT_NAME, identityManager);
}
@SuppressWarnings("deprecation")
private IdentityManager getIdentityManager(SecurityContext securityContext) {
return identityManager != null ? identityManager : securityContext.getIdentityManager();
}
public AuthenticationMechanismOutcome authenticate(final HttpServerExchange exchange,
final SecurityContext securityContext) {
List<String> authHeaders = exchange.getRequestHeaders().get(AUTHORIZATION);
if (authHeaders != null) {
for (String current : authHeaders) {
if (current.startsWith(DIGEST_PREFIX)) {
String digestChallenge = current.substring(PREFIX_LENGTH);
try {
DigestContext context = new DigestContext();
Map<DigestAuthorizationToken, String> parsedHeader = parseHeader(digestChallenge);
context.setMethod(exchange.getRequestMethod().toString());
context.setParsedHeader(parsedHeader);
// Some form of Digest authentication is going to occur so get the DigestContext set on the exchange.
exchange.putAttachment(DigestContext.ATTACHMENT_KEY, context);
UndertowLogger.SECURITY_LOGGER.debugf("Found digest header %s in %s", current, exchange);
return handleDigestHeader(exchange, securityContext);
} catch (Exception e) {
UndertowLogger.SECURITY_LOGGER.authenticationFailedFor(current, exchange, e);
}
}
// By this point we had a header we should have been able to verify but for some reason
// it was not correctly structured.
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
}
// No suitable header has been found in this request,
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
private AuthenticationMechanismOutcome handleDigestHeader(HttpServerExchange exchange, final SecurityContext securityContext) {
DigestContext context = exchange.getAttachment(DigestContext.ATTACHMENT_KEY);
Map<DigestAuthorizationToken, String> parsedHeader = context.getParsedHeader();
// Step 1 - Verify the set of tokens received to ensure valid values.
Set<DigestAuthorizationToken> mandatoryTokens = EnumSet.copyOf(MANDATORY_REQUEST_TOKENS);
if (!supportedAlgorithms.contains(DigestAlgorithm.MD5)) {
// If we don't support MD5 then the client must choose an algorithm as we can not fall back to MD5.
mandatoryTokens.add(DigestAuthorizationToken.ALGORITHM);
}
if (!supportedQops.isEmpty() && !supportedQops.contains(DigestQop.AUTH)) {
// If we do not support auth then we are mandating auth-int so force the client to send a QOP
mandatoryTokens.add(DigestAuthorizationToken.MESSAGE_QOP);
}
DigestQop qop = null;
// This check is early as is increases the list of mandatory tokens.
if (parsedHeader.containsKey(DigestAuthorizationToken.MESSAGE_QOP)) {
qop = DigestQop.forName(parsedHeader.get(DigestAuthorizationToken.MESSAGE_QOP));
if (qop == null || !supportedQops.contains(qop)) {
// We are also ensuring the client is not trying to force a qop that has been disabled.
REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.MESSAGE_QOP.getName(),
parsedHeader.get(DigestAuthorizationToken.MESSAGE_QOP));
// TODO - This actually needs to result in a HTTP 400 Bad Request response and not a new challenge.
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
context.setQop(qop);
mandatoryTokens.add(DigestAuthorizationToken.CNONCE);
mandatoryTokens.add(DigestAuthorizationToken.NONCE_COUNT);
}
// Check all mandatory tokens are present.
mandatoryTokens.removeAll(parsedHeader.keySet());
if (mandatoryTokens.size() > 0) {
for (DigestAuthorizationToken currentToken : mandatoryTokens) {
// TODO - Need a better check and possible concatenate the list of tokens - however
// even having one missing token is not something we should routinely expect.
REQUEST_LOGGER.missingAuthorizationToken(currentToken.getName());
}
// TODO - This actually needs to result in a HTTP 400 Bad Request response and not a new challenge.
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
// Perform some validation of the remaining tokens.
if (!realmName.equals(parsedHeader.get(DigestAuthorizationToken.REALM))) {
REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.REALM.getName(),
parsedHeader.get(DigestAuthorizationToken.REALM));
// TODO - This actually needs to result in a HTTP 400 Bad Request response and not a new challenge.
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
if(parsedHeader.containsKey(DigestAuthorizationToken.DIGEST_URI)) {
String uri = parsedHeader.get(DigestAuthorizationToken.DIGEST_URI);
String requestURI = exchange.getRequestURI();
if(!exchange.getQueryString().isEmpty()) {
requestURI = requestURI + "?" + exchange.getQueryString();
}
if(!uri.equals(requestURI)) {
//it is possible we were given an absolute URI
//we reconstruct the URI from the host header to make sure they match up
//I am not sure if this is overly strict, however I think it is better
//to be safe than sorry
requestURI = exchange.getRequestURL();
if(!exchange.getQueryString().isEmpty()) {
requestURI = requestURI + "?" + exchange.getQueryString();
}
if(!uri.equals(requestURI)) {
//just end the auth process
exchange.setStatusCode(StatusCodes.BAD_REQUEST);
exchange.endExchange();
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
}
} else {
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
if (parsedHeader.containsKey(DigestAuthorizationToken.OPAQUE)) {
if (!OPAQUE_VALUE.equals(parsedHeader.get(DigestAuthorizationToken.OPAQUE))) {
REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.OPAQUE.getName(),
parsedHeader.get(DigestAuthorizationToken.OPAQUE));
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
}
DigestAlgorithm algorithm;
if (parsedHeader.containsKey(DigestAuthorizationToken.ALGORITHM)) {
algorithm = DigestAlgorithm.forName(parsedHeader.get(DigestAuthorizationToken.ALGORITHM));
if (algorithm == null || !supportedAlgorithms.contains(algorithm)) {
// We are also ensuring the client is not trying to force an algorithm that has been disabled.
REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.ALGORITHM.getName(),
parsedHeader.get(DigestAuthorizationToken.ALGORITHM));
// TODO - This actually needs to result in a HTTP 400 Bad Request response and not a new challenge.
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
} else {
// We know this is safe as the algorithm token was made mandatory
// if MD5 is not supported.
algorithm = DigestAlgorithm.MD5;
}
try {
context.setAlgorithm(algorithm);
} catch (NoSuchAlgorithmException e) {
/*
* This should not be possible in a properly configured installation.
*/
REQUEST_LOGGER.exceptionProcessingRequest(e);
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
final String userName = parsedHeader.get(DigestAuthorizationToken.USERNAME);
final IdentityManager identityManager = getIdentityManager(securityContext);
final Account account;
if (algorithm.isSession()) {
/* This can follow one of the following: -
* 1 - New session so use DigestCredentialImpl with the IdentityManager to
* create a new session key.
* 2 - Obtain the existing session key from the session store and validate it, just use
* IdentityManager to validate account is still active and the current role assignment.
*/
throw new IllegalStateException("Not yet implemented.");
} else {
final DigestCredential credential = new DigestCredentialImpl(context);
account = identityManager.verify(userName, credential);
}
if (account == null) {
// Authentication has failed, this could either be caused by the user not-existing or it
// could be caused due to an invalid hash.
securityContext.authenticationFailed(MESSAGES.authenticationFailed(userName), mechanismName);
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
// Step 3 - Verify that the nonce was eligible to be used.
if (!validateNonceUse(context, parsedHeader, exchange)) {
// TODO - This is the right place to make use of the decision but the check needs to be much much sooner
// otherwise a failure server
// side could leave a packet that could be 're-played' after the failed auth.
// The username and password verification passed but for some reason we do not like the nonce.
context.markStale();
// We do not mark as a failure on the security context as this is not quite a failure, a client with a cached nonce
// can easily hit this point.
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
// We have authenticated the remote user.
sendAuthenticationInfoHeader(exchange);
securityContext.authenticationComplete(account, mechanismName, false);
return AuthenticationMechanismOutcome.AUTHENTICATED;
// Step 4 - Set up any QOP related requirements.
// TODO - Do QOP
}
private boolean validateRequest(final DigestContext context, final byte[] ha1) {
byte[] ha2;
DigestQop qop = context.getQop();
// Step 2.2 Calculate H(A2)
if (qop == null || qop.equals(DigestQop.AUTH)) {
ha2 = createHA2Auth(context, context.getParsedHeader());
} else {
ha2 = createHA2AuthInt();
}
byte[] requestDigest;
if (qop == null) {
requestDigest = createRFC2069RequestDigest(ha1, ha2, context);
} else {
requestDigest = createRFC2617RequestDigest(ha1, ha2, context);
}
byte[] providedResponse = context.getParsedHeader().get(DigestAuthorizationToken.RESPONSE).getBytes(StandardCharsets.UTF_8);
return MessageDigest.isEqual(requestDigest, providedResponse);
}
private boolean validateNonceUse(DigestContext context, Map<DigestAuthorizationToken, String> parsedHeader, final HttpServerExchange exchange) {
String suppliedNonce = parsedHeader.get(DigestAuthorizationToken.NONCE);
int nonceCount = -1;
if (parsedHeader.containsKey(DigestAuthorizationToken.NONCE_COUNT)) {
String nonceCountHex = parsedHeader.get(DigestAuthorizationToken.NONCE_COUNT);
nonceCount = Integer.parseInt(nonceCountHex, 16);
}
context.setNonce(suppliedNonce);
// TODO - A replay attempt will need an exception.
return (nonceManager.validateNonce(suppliedNonce, nonceCount, exchange));
}
private byte[] createHA2Auth(final DigestContext context, Map<DigestAuthorizationToken, String> parsedHeader) {
byte[] method = context.getMethod().getBytes(StandardCharsets.UTF_8);
byte[] digestUri = parsedHeader.get(DigestAuthorizationToken.DIGEST_URI).getBytes(StandardCharsets.UTF_8);
MessageDigest digest = context.getDigest();
try {
digest.update(method);
digest.update(COLON);
digest.update(digestUri);
return HexConverter.convertToHexBytes(digest.digest());
} finally {
digest.reset();
}
}
private byte[] createHA2AuthInt() {
// TODO - Implement method.
throw new IllegalStateException("Method not implemented.");
}
private byte[] createRFC2069RequestDigest(final byte[] ha1, final byte[] ha2, final DigestContext context) {
final MessageDigest digest = context.getDigest();
final Map<DigestAuthorizationToken, String> parsedHeader = context.getParsedHeader();
byte[] nonce = parsedHeader.get(DigestAuthorizationToken.NONCE).getBytes(StandardCharsets.UTF_8);
try {
digest.update(ha1);
digest.update(COLON);
digest.update(nonce);
digest.update(COLON);
digest.update(ha2);
return HexConverter.convertToHexBytes(digest.digest());
} finally {
digest.reset();
}
}
private byte[] createRFC2617RequestDigest(final byte[] ha1, final byte[] ha2, final DigestContext context) {
final MessageDigest digest = context.getDigest();
final Map<DigestAuthorizationToken, String> parsedHeader = context.getParsedHeader();
byte[] nonce = parsedHeader.get(DigestAuthorizationToken.NONCE).getBytes(StandardCharsets.UTF_8);
byte[] nonceCount = parsedHeader.get(DigestAuthorizationToken.NONCE_COUNT).getBytes(StandardCharsets.UTF_8);
byte[] cnonce = parsedHeader.get(DigestAuthorizationToken.CNONCE).getBytes(StandardCharsets.UTF_8);
byte[] qop = parsedHeader.get(DigestAuthorizationToken.MESSAGE_QOP).getBytes(StandardCharsets.UTF_8);
try {
digest.update(ha1);
digest.update(COLON);
digest.update(nonce);
digest.update(COLON);
digest.update(nonceCount);
digest.update(COLON);
digest.update(cnonce);
digest.update(COLON);
digest.update(qop);
digest.update(COLON);
digest.update(ha2);
return HexConverter.convertToHexBytes(digest.digest());
} finally {
digest.reset();
}
}
@Override
public ChallengeResult sendChallenge(final HttpServerExchange exchange, final SecurityContext securityContext) {
// Ensure a session is created to have stickiness through loadbalancers
try {
Sessions.getOrCreateSession(exchange);
} catch (IllegalStateException e) {
UndertowLogger.SECURITY_LOGGER.debugf("Session error. Digest auth may fail from broken stickiness", e);
}
DigestContext context = exchange.getAttachment(DigestContext.ATTACHMENT_KEY);
boolean stale = context == null ? false : context.isStale();
StringBuilder rb = new StringBuilder(DIGEST_PREFIX);
rb.append(Headers.REALM.toString()).append("=\"").append(realmName).append("\",");
rb.append(Headers.DOMAIN.toString()).append("=\"").append(domain).append("\",");
// based on security constraints.
rb.append(Headers.NONCE.toString()).append("=\"").append(nonceManager.nextNonce(null, exchange)).append("\",");
// Not currently using OPAQUE as it offers no integrity, used for session data leaves it vulnerable to
// session fixation type issues as well.
rb.append(Headers.OPAQUE.toString()).append("=\"00000000000000000000000000000000\"");
if (stale) {
rb.append(",stale=true");
}
if (supportedAlgorithms.size() > 0) {
// This header will need to be repeated once for each algorithm.
rb.append(",").append(Headers.ALGORITHM.toString()).append("=%s");
}
if (qopString != null) {
rb.append(",").append(Headers.QOP.toString()).append("=\"").append(qopString).append("\"");
}
String theChallenge = rb.toString();
HeaderMap responseHeader = exchange.getResponseHeaders();
if (supportedAlgorithms.isEmpty()) {
responseHeader.add(WWW_AUTHENTICATE, theChallenge);
} else {
for (DigestAlgorithm current : supportedAlgorithms) {
responseHeader.add(WWW_AUTHENTICATE, String.format(theChallenge, current.getToken()));
}
}
return new ChallengeResult(true, UNAUTHORIZED);
}
public void sendAuthenticationInfoHeader(final HttpServerExchange exchange) {
DigestContext context = exchange.getAttachment(DigestContext.ATTACHMENT_KEY);
DigestQop qop = context.getQop();
String currentNonce = context.getNonce();
String nextNonce = nonceManager.nextNonce(currentNonce, exchange);
if (qop != null || !nextNonce.equals(currentNonce)) {
StringBuilder sb = new StringBuilder();
sb.append(NEXT_NONCE).append("=\"").append(nextNonce).append("\"");
if (qop != null) {
Map<DigestAuthorizationToken, String> parsedHeader = context.getParsedHeader();
sb.append(",").append(Headers.QOP.toString()).append("=\"").append(qop.getToken()).append("\"");
byte[] ha1 = context.getHa1();
byte[] ha2;
if (qop == DigestQop.AUTH) {
ha2 = createHA2Auth(context);
} else {
ha2 = createHA2AuthInt();
}
String rspauth = new String(createRFC2617RequestDigest(ha1, ha2, context), StandardCharsets.UTF_8);
sb.append(",").append(Headers.RESPONSE_AUTH.toString()).append("=\"").append(rspauth).append("\"");
sb.append(",").append(Headers.CNONCE.toString()).append("=\"").append(parsedHeader.get(DigestAuthorizationToken.CNONCE)).append("\"");
sb.append(",").append(Headers.NONCE_COUNT.toString()).append("=").append(parsedHeader.get(DigestAuthorizationToken.NONCE_COUNT));
}
HeaderMap responseHeader = exchange.getResponseHeaders();
responseHeader.add(AUTHENTICATION_INFO, sb.toString());
}
exchange.removeAttachment(DigestContext.ATTACHMENT_KEY);
}
private byte[] createHA2Auth(final DigestContext context) {
byte[] digestUri = context.getParsedHeader().get(DigestAuthorizationToken.DIGEST_URI).getBytes(StandardCharsets.UTF_8);
MessageDigest digest = context.getDigest();
try {
digest.update(COLON);
digest.update(digestUri);
return HexConverter.convertToHexBytes(digest.digest());
} finally {
digest.reset();
}
}
private static class DigestContext {
static final AttachmentKey<DigestContext> ATTACHMENT_KEY = AttachmentKey.create(DigestContext.class);
private String method;
private String nonce;
private DigestQop qop;
private byte[] ha1;
private DigestAlgorithm algorithm;
private MessageDigest digest;
private boolean stale = false;
Map<DigestAuthorizationToken, String> parsedHeader;
String getMethod() {
return method;
}
void setMethod(String method) {
this.method = method;
}
boolean isStale() {
return stale;
}
void markStale() {
this.stale = true;
}
String getNonce() {
return nonce;
}
void setNonce(String nonce) {
this.nonce = nonce;
}
DigestQop getQop() {
return qop;
}
void setQop(DigestQop qop) {
this.qop = qop;
}
byte[] getHa1() {
return ha1;
}
void setHa1(byte[] ha1) {
this.ha1 = ha1;
}
DigestAlgorithm getAlgorithm() {
return algorithm;
}
void setAlgorithm(DigestAlgorithm algorithm) throws NoSuchAlgorithmException {
this.algorithm = algorithm;
digest = algorithm.getMessageDigest();
}
MessageDigest getDigest() {
return digest;
}
Map<DigestAuthorizationToken, String> getParsedHeader() {
return parsedHeader;
}
void setParsedHeader(Map<DigestAuthorizationToken, String> parsedHeader) {
this.parsedHeader = parsedHeader;
}
}
private class DigestCredentialImpl implements DigestCredential {
private final DigestContext context;
private DigestCredentialImpl(final DigestContext digestContext) {
this.context = digestContext;
}
@Override
public DigestAlgorithm getAlgorithm() {
return context.getAlgorithm();
}
@Override
public boolean verifyHA1(byte[] ha1) {
context.setHa1(ha1); // Cache for subsequent use.
return validateRequest(context, ha1);
}
@Override
public String getRealm() {
return realmName;
}
@Override
public byte[] getSessionData() {
if (!context.getAlgorithm().isSession()) {
throw MESSAGES.noSessionData();
}
byte[] nonce = context.getParsedHeader().get(DigestAuthorizationToken.NONCE).getBytes(StandardCharsets.UTF_8);
byte[] cnonce = context.getParsedHeader().get(DigestAuthorizationToken.CNONCE).getBytes(StandardCharsets.UTF_8);
byte[] response = new byte[nonce.length + cnonce.length + 1];
System.arraycopy(nonce, 0, response, 0, nonce.length);
response[nonce.length] = ':';
System.arraycopy(cnonce, 0, response, nonce.length + 1, cnonce.length);
return response;
}
}
public static final class Factory implements AuthenticationMechanismFactory {
@Deprecated
public Factory(IdentityManager identityManager) {}
public Factory() {}
@Override
public AuthenticationMechanism create(String mechanismName,IdentityManager identityManager, FormParserFactory formParserFactory, Map<String, String> properties) {
return new DigestAuthenticationMechanism(properties.get(REALM), properties.get(CONTEXT_PATH), mechanismName, identityManager);
}
}
}