/*
 * 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 <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) {
        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);
        }
    }

}
