io.undertow.security.impl.DigestAuthenticationMechanism Maven / Gradle / Ivy
Go to download
This artifact provides a single jar that contains all classes required to use remote EJB and JMS, including
all dependencies. It is intended for use by those not using maven, maven users should just import the EJB and
JMS BOM's instead (shaded JAR's cause lots of problems with maven, as it is very easy to inadvertently end up
with different versions on classes on the class path).
/*
* 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.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 Darran Lofthouse
*/
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 MANDATORY_REQUEST_TOKENS;
static {
Set 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 supportedAlgorithms;
private final List 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 supportedAlgorithms, final List supportedQops,
final String realmName, final String domain, final NonceManager nonceManager) {
this(supportedAlgorithms, supportedQops, realmName, domain, nonceManager, DEFAULT_NAME);
}
public DigestAuthenticationMechanism(final List supportedAlgorithms, final List 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 supportedAlgorithms, final List 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 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 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 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 parsedHeader = context.getParsedHeader();
// Step 1 - Verify the set of tokens received to ensure valid values.
Set 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 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 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 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 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) {
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 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 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 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 getParsedHeader() {
return parsedHeader;
}
void setParsedHeader(Map 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 properties) {
return new DigestAuthenticationMechanism(properties.get(REALM), properties.get(CONTEXT_PATH), mechanismName, identityManager);
}
}
}