io.undertow.security.impl.DigestAuthenticationMechanism Maven / Gradle / Ivy
/*
* 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);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy