1
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
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
92 private final List<DigestAlgorithm> supportedAlgorithms;
93 private final List<DigestQop> supportedQops;
94 private final String qopString;
95 private final String realmName;
96 private final String domain;
97 private final NonceManager nonceManager;
98
99
100
101
102
103
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
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
176
177 return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
178 }
179 }
180
181
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
189 Set<DigestAuthorizationToken> mandatoryTokens = EnumSet.copyOf(MANDATORY_REQUEST_TOKENS);
190 if (!supportedAlgorithms.contains(DigestAlgorithm.MD5)) {
191
192 mandatoryTokens.add(DigestAuthorizationToken.ALGORITHM);
193 }
194 if (!supportedQops.isEmpty() && !supportedQops.contains(DigestQop.AUTH)) {
195
196 mandatoryTokens.add(DigestAuthorizationToken.MESSAGE_QOP);
197 }
198
199 DigestQop qop = null;
200
201 if (parsedHeader.containsKey(DigestAuthorizationToken.MESSAGE_QOP)) {
202 qop = DigestQop.forName(parsedHeader.get(DigestAuthorizationToken.MESSAGE_QOP));
203 if (qop == null || !supportedQops.contains(qop)) {
204
205 REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.MESSAGE_QOP.getName(),
206 parsedHeader.get(DigestAuthorizationToken.MESSAGE_QOP));
207
208 return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
209 }
210 context.setQop(qop);
211 mandatoryTokens.add(DigestAuthorizationToken.CNONCE);
212 mandatoryTokens.add(DigestAuthorizationToken.NONCE_COUNT);
213 }
214
215
216 mandatoryTokens.removeAll(parsedHeader.keySet());
217 if (mandatoryTokens.size() > 0) {
218 for (DigestAuthorizationToken currentToken : mandatoryTokens) {
219
220
221 REQUEST_LOGGER.missingAuthorizationToken(currentToken.getName());
222 }
223
224 return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
225 }
226
227
228 if (!realmName.equals(parsedHeader.get(DigestAuthorizationToken.REALM))) {
229 REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.REALM.getName(),
230 parsedHeader.get(DigestAuthorizationToken.REALM));
231
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
243
244
245
246 requestURI = exchange.getRequestURL();
247 if(!exchange.getQueryString().isEmpty()) {
248 requestURI = requestURI + "?" + exchange.getQueryString();
249 }
250 if(!uri.equals(requestURI)) {
251
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
274 REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.ALGORITHM.getName(),
275 parsedHeader.get(DigestAuthorizationToken.ALGORITHM));
276
277 return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
278 }
279 } else {
280
281
282 algorithm = DigestAlgorithm.MD5;
283 }
284
285 try {
286 context.setAlgorithm(algorithm);
287 } catch (NoSuchAlgorithmException e) {
288
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
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
314
315 securityContext.authenticationFailed(MESSAGES.authenticationFailed(userName), mechanismName);
316 return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
317 }
318
319
320 if (!validateNonceUse(context, parsedHeader, exchange)) {
321
322
323
324
325 context.markStale();
326
327
328 return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
329 }
330
331
332
333 sendAuthenticationInfoHeader(exchange);
334 securityContext.authenticationComplete(account, mechanismName, false);
335 return AuthenticationMechanismOutcome.AUTHENTICATED;
336
337
338
339
340 }
341
342 private boolean validateRequest(final DigestContext context, final byte[] ha1) {
343 byte[] ha2;
344 DigestQop qop = context.getQop();
345
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
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
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
455 rb.append(Headers.NONCE.toString()).append("=\"").append(nonceManager.nextNonce(null, exchange)).append("\",");
456
457
458 rb.append(Headers.OPAQUE.toString()).append("=\"00000000000000000000000000000000\"");
459 if (stale) {
460 rb.append(",stale=true");
461 }
462 if (supportedAlgorithms.size() > 0) {
463
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);
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