/*
* JBoss, Home of Professional Open Source.
* Copyright 2016 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 org.wildfly.security.http.impl;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.wildfly.security._private.ElytronMessages.log;
import static org.wildfly.security.http.HttpConstants.ALGORITHM;
import static org.wildfly.security.http.HttpConstants.AUTHORIZATION;
import static org.wildfly.security.http.HttpConstants.DIGEST_NAME;
import static org.wildfly.security.http.HttpConstants.URI;
import static org.wildfly.security.http.HttpConstants.DOMAIN;
import static org.wildfly.security.http.HttpConstants.MD5;
import static org.wildfly.security.http.HttpConstants.NONCE;
import static org.wildfly.security.http.HttpConstants.OPAQUE;
import static org.wildfly.security.http.HttpConstants.REALM;
import static org.wildfly.security.http.HttpConstants.RESPONSE;
import static org.wildfly.security.http.HttpConstants.STALE;
import static org.wildfly.security.http.HttpConstants.UNAUTHORIZED;
import static org.wildfly.security.http.HttpConstants.USERNAME;
import static org.wildfly.security.http.HttpConstants.WWW_AUTHENTICATE;
import static org.wildfly.security.mechanism.digest.DigestUtil.getTwoWayPasswordChars;
import static org.wildfly.security.mechanism.digest.DigestUtil.parseResponse;
import static org.wildfly.security.mechanism.digest.DigestUtil.userRealmPasswordDigest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.function.Supplier;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.RealmCallback;
import org.wildfly.security._private.ElytronMessages;
import org.wildfly.security.auth.callback.AuthenticationCompleteCallback;
import org.wildfly.security.auth.callback.AvailableRealmsCallback;
import org.wildfly.security.auth.callback.CredentialCallback;
import org.wildfly.security.credential.PasswordCredential;
import org.wildfly.security.http.HttpAuthenticationException;
import org.wildfly.security.http.HttpConstants;
import org.wildfly.security.http.HttpServerAuthenticationMechanism;
import org.wildfly.security.http.HttpServerRequest;
import org.wildfly.security.http.HttpServerResponse;
import org.wildfly.security.mechanism.AuthenticationMechanismException;
import org.wildfly.security.mechanism.digest.DigestQuote;
import org.wildfly.security.password.TwoWayPassword;
import org.wildfly.security.password.interfaces.ClearPassword;
import org.wildfly.security.password.interfaces.DigestPassword;
import org.wildfly.security.util.ByteIterator;
/**
* Implementation of the HTTP DIGEST authentication mechanism.
*
* @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a>
*/
class DigestAuthenticationMechanism implements HttpServerAuthenticationMechanism {
private static final String CHALLENGE_PREFIX = "Digest ";
private static final int PREFIX_LENGTH = CHALLENGE_PREFIX.length();
private static final String OPAQUE_VALUE = "00000000000000000000000000000000";
private static final byte COLON = ':';
private final Supplier<Provider[]> providers;
private final CallbackHandler callbackHandler;
private final NonceManager nonceManager;
private final String configuredRealm;
private final String domain;
/**
*
* @param callbackHandler
* @param nonceManager
* @param configuredRealm
*/
DigestAuthenticationMechanism(CallbackHandler callbackHandler, NonceManager nonceManager, String configuredRealm, String domain, Supplier<Provider[]> providers) {
this.callbackHandler = callbackHandler;
this.nonceManager = nonceManager;
this.configuredRealm = configuredRealm;
this.domain = domain;
this.providers = providers;
}
@Override
public String getMechanismName() {
return DIGEST_NAME;
}
@Override
public void evaluateRequest(final HttpServerRequest request) throws HttpAuthenticationException {
List<String> authorizationValues = request.getRequestHeaderValues(AUTHORIZATION);
if (authorizationValues != null) {
for (String current : authorizationValues) {
if (current.startsWith(CHALLENGE_PREFIX)) {
byte[] rawHeader = current.substring(CHALLENGE_PREFIX.length()).getBytes(UTF_8);
try {
HashMap<String, byte[]> responseTokens = parseResponse(rawHeader, UTF_8, false, getMechanismName());
validateResponse(responseTokens, request);
return;
} catch (AuthenticationMechanismException e) {
log.trace(e);
request.badRequest(e.toHttpAuthenticationException(), response -> prepareResponse(selectRealm(), response, false));
return;
}
}
}
}
request.noAuthenticationInProgress(response -> prepareResponse(selectRealm(), response, false));
}
private void validateResponse(HashMap<String, byte[]> responseTokens, final HttpServerRequest request) throws AuthenticationMechanismException, HttpAuthenticationException {
String nonce = convertToken(NONCE, responseTokens.get(NONCE));
String messageRealm = convertToken(REALM, responseTokens.get(REALM));
/*
* We want to get the nonce checkes ASAP so it is recorded as used in case some intermittent failure prevents validation.
*
* We act on the validity at the end where we can let the client know if it is stale.
*/
boolean nonceValid = nonceManager.useNonce(nonce, messageRealm.getBytes(UTF_8));
String username = convertToken(USERNAME, responseTokens.get(USERNAME));
byte[] digestUri;
if (responseTokens.containsKey(URI)) {
digestUri = responseTokens.get(URI);
} else {
throw log.mechMissingDirective(getMechanismName(), URI);
}
byte[] response;
if (responseTokens.containsKey(RESPONSE)) {
response = ByteIterator.ofBytes(responseTokens.get(RESPONSE)).hexDecode().drain();
} else {
throw log.mechMissingDirective(getMechanismName(), RESPONSE);
}
String algorithm = convertToken(ALGORITHM, responseTokens.get(ALGORITHM));
if (MD5.equals(algorithm) == false) {
throw log.mechUnsupportedAlgorithm(getMechanismName(), algorithm);
}
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance(MD5);
} catch (NoSuchAlgorithmException e) {
throw log.mechMacAlgorithmNotSupported(getMechanismName(), e);
}
if (!checkRealm(messageRealm)) {
throw log.mechDisallowedClientRealm(getMechanismName(), messageRealm);
}
String selectedRealm = selectRealm();
if (username.length() == 0) {
fail();
request.authenticationFailed(log.authenticationFailed(getMechanismName()), httpResponse -> prepareResponse(selectedRealm, httpResponse, false));
return;
}
byte[] hA1 = getH_A1(messageDigest, username, messageRealm);
if (hA1 == null) {
fail();
request.authenticationFailed(log.authenticationFailed(getMechanismName()), httpResponse -> prepareResponse(selectedRealm, httpResponse, false));
return;
}
byte[] calculatedResponse = calculateResponseDigest(messageDigest, hA1, nonce, request.getRequestMethod(), digestUri);
if (Arrays.equals(response, calculatedResponse) == false) {
fail();
request.authenticationFailed(log.mechResponseTokenMismatch(getMechanismName()), httpResponse -> prepareResponse(selectedRealm, httpResponse, false));
return;
}
if (nonceValid == false) {
request.authenticationInProgress(httpResponse -> prepareResponse(selectedRealm, httpResponse, true));
return;
}
if (authorize(username)) {
succeed();
request.authenticationComplete();
} else {
fail();
request.authenticationFailed(log.authorizationFailed(username, getMechanismName()), httpResponse -> httpResponse.setStatusCode(HttpConstants.FORBIDDEN));
}
}
/**
* Check if realm is offered by the server
*/
private boolean checkRealm(String realm) throws AuthenticationMechanismException {
String[] realms = getAvailableRealms();
if (realms != null) {
for (String current : realms) {
if (realm.equals(current)) {
return true;
}
}
}
return false;
}
private byte[] calculateResponseDigest(MessageDigest messageDigest, byte[] hA1, String nonce, String method, byte[] digestUri) {
messageDigest.update(method.getBytes(UTF_8));
messageDigest.update(COLON);
byte[] hA2 = messageDigest.digest(digestUri);
messageDigest.update(ByteIterator.ofBytes(hA1).hexEncode().drainToString().getBytes(UTF_8));
messageDigest.update(COLON);
messageDigest.update(nonce.getBytes(UTF_8));
messageDigest.update(COLON);
return messageDigest.digest(ByteIterator.ofBytes(hA2).hexEncode().drainToString().getBytes(UTF_8));
}
private byte[] getH_A1(final MessageDigest messageDigest, final String username, final String messageRealm) throws AuthenticationMechanismException {
final NameCallback nameCallback = new NameCallback("User name", username);
final RealmCallback realmCallback = new RealmCallback("User realm", messageRealm);
byte[] response = null;
// The mechanism configuration understands the realm name so fully pre-digested may be possible.
response = getPredigestedSaltedPassword(realmCallback, nameCallback, DigestPassword.ALGORITHM_DIGEST_MD5, getMechanismName());
if (response != null) {
return response;
}
response = getSaltedPasswordFromTwoWay(messageDigest, realmCallback, nameCallback);
if (response != null) {
return response;
}
response = getSaltedPasswordFromPasswordCallback(messageDigest, realmCallback, nameCallback);
return response;
}
private String convertToken(final String name, final byte[] value) throws AuthenticationMechanismException {
if (value == null) {
throw log.mechMissingDirective(getMechanismName(), name);
}
return new String(value, UTF_8);
}
/**
* Select the realm which should be sent to the client in the challenge.
*
* If a realm has been configured it takes priority.
* Next the first available mechanism realm is selected.
* If no mechanism is available or mechanism configured realm is not offered by the server, {@link IllegalStateException} is thrown.
* @throws HttpAuthenticationException
*
*/
private String selectRealm() throws HttpAuthenticationException {
try {
if (configuredRealm != null) {
if (!checkRealm(configuredRealm)) {
throw log.digestMechanismInvalidRealm(configuredRealm);
}
return configuredRealm;
}
String[] realms = getAvailableRealms();
if (realms != null && realms.length > 0) {
return realms[0];
}
throw log.digestMechanismRequireRealm();
} catch (AuthenticationMechanismException e) {
throw e.toHttpAuthenticationException();
}
}
private String[] getAvailableRealms() throws AuthenticationMechanismException {
final AvailableRealmsCallback availableRealmsCallback = new AvailableRealmsCallback();
try {
callbackHandler.handle(new Callback[] { availableRealmsCallback });
return availableRealmsCallback.getRealmNames();
} catch (UnsupportedCallbackException ignored) {
return new String[0];
} catch (AuthenticationMechanismException e) {
throw e;
} catch (IOException e) {
throw ElytronMessages.log.mechCallbackHandlerFailedForUnknownReason(DIGEST_NAME, e);
}
}
private void prepareResponse(String realmName, HttpServerResponse response, boolean stale) throws HttpAuthenticationException {
StringBuilder sb = new StringBuilder(CHALLENGE_PREFIX);
sb.append(REALM).append("=\"").append(DigestQuote.quote(realmName)).append("\"");
if (domain != null) {
sb.append(", ").append(DOMAIN).append("=\"").append(domain).append("\"");
}
sb.append(", ").append(NONCE).append("=\"").append(nonceManager.generateNonce(realmName.getBytes(StandardCharsets.UTF_8))).append("\"");
sb.append(", ").append(OPAQUE).append("=\"").append(OPAQUE_VALUE).append("\"");
if (stale) {
sb.append(", ").append(STALE).append("=true");
}
sb.append(", ").append(ALGORITHM).append("=").append(MD5);
response.addResponseHeader(WWW_AUTHENTICATE, sb.toString());
response.setStatusCode(UNAUTHORIZED);
}
public byte[] getPredigestedSaltedPassword(RealmCallback realmCallback, NameCallback nameCallback, String passwordAlgorithm, String mechanismName) throws AuthenticationMechanismException {
CredentialCallback credentialCallback = new CredentialCallback(PasswordCredential.class, passwordAlgorithm);
try {
callbackHandler.handle(new Callback[] { realmCallback, nameCallback, credentialCallback });
return credentialCallback.applyToCredential(PasswordCredential.class, c -> c.getPassword().castAndApply(DigestPassword.class, DigestPassword::getDigest));
} catch (UnsupportedCallbackException e) {
if (e.getCallback() == credentialCallback) {
return null;
} else if (e.getCallback() == nameCallback) {
throw log.mechCallbackHandlerDoesNotSupportUserName(mechanismName, e);
} else {
throw log.mechCallbackHandlerFailedForUnknownReason(mechanismName, e);
}
} catch (Throwable t) {
throw log.mechCallbackHandlerFailedForUnknownReason(mechanismName, t);
}
}
protected byte[] getSaltedPasswordFromTwoWay(MessageDigest messageDigest, RealmCallback realmCallback, NameCallback nameCallback) throws AuthenticationMechanismException {
CredentialCallback credentialCallback = new CredentialCallback(PasswordCredential.class, ClearPassword.ALGORITHM_CLEAR);
try {
callbackHandler.handle(new Callback[] {realmCallback, nameCallback, credentialCallback});
} catch (UnsupportedCallbackException e) {
if (e.getCallback() == credentialCallback) {
return null;
} else if (e.getCallback() == nameCallback) {
throw log.mechCallbackHandlerDoesNotSupportUserName(getMechanismName(), e);
} else {
throw log.mechCallbackHandlerFailedForUnknownReason(getMechanismName(), e);
}
} catch (Throwable t) {
throw log.mechCallbackHandlerFailedForUnknownReason(getMechanismName(), t);
}
TwoWayPassword password = credentialCallback.applyToCredential(PasswordCredential.class, c -> c.getPassword().castAs(TwoWayPassword.class));
char[] passwordChars = getTwoWayPasswordChars(getMechanismName(), password, providers);
try {
password.destroy();
} catch(DestroyFailedException e) {
log.credentialDestroyingFailed(e);
}
String realm = realmCallback.getDefaultText();
String username = nameCallback.getDefaultName();
byte[] digest_urp = userRealmPasswordDigest(messageDigest, username, realm, passwordChars);
Arrays.fill(passwordChars, (char)0); // wipe out the password
return digest_urp;
}
protected byte[] getSaltedPasswordFromPasswordCallback(MessageDigest messageDigest, RealmCallback realmCallback, NameCallback nameCallback) throws AuthenticationMechanismException {
PasswordCallback passwordCallback = new PasswordCallback("User password", false);
try {
callbackHandler.handle(new Callback[] {realmCallback, nameCallback, passwordCallback});
} catch (UnsupportedCallbackException e) {
if (e.getCallback() == passwordCallback) {
return null;
} else if (e.getCallback() == nameCallback) {
throw log.mechCallbackHandlerDoesNotSupportUserName(getMechanismName(), e);
} else {
throw log.mechCallbackHandlerFailedForUnknownReason(getMechanismName(), e);
}
} catch (Throwable t) {
throw log.mechCallbackHandlerFailedForUnknownReason(getMechanismName(), t);
}
char[] passwordChars = passwordCallback.getPassword();
passwordCallback.clearPassword();
if (passwordChars == null) {
throw log.mechNoPasswordGiven(getMechanismName());
}
String realm = realmCallback.getDefaultText();
String username = nameCallback.getDefaultName();
byte[] digest_urp = userRealmPasswordDigest(messageDigest, username, realm, passwordChars);
Arrays.fill(passwordChars, (char)0); // wipe out the password
return digest_urp;
}
protected boolean authorize(String username) throws AuthenticationMechanismException {
AuthorizeCallback authorizeCallback = new AuthorizeCallback(username, username);
try {
callbackHandler.handle(new Callback[] {authorizeCallback});
return authorizeCallback.isAuthorized();
} catch (UnsupportedCallbackException e) {
return false;
} catch (Throwable t) {
throw log.mechCallbackHandlerFailedForUnknownReason(getMechanismName(), t);
}
}
protected void succeed() throws AuthenticationMechanismException {
try {
callbackHandler.handle(new Callback[] { AuthenticationCompleteCallback.SUCCEEDED });
} catch (Throwable t) {
throw log.mechCallbackHandlerFailedForUnknownReason(getMechanismName(), t);
}
}
protected void fail() throws AuthenticationMechanismException {
try {
callbackHandler.handle(new Callback[] { AuthenticationCompleteCallback.FAILED });
} catch (Throwable t) {
throw log.mechCallbackHandlerFailedForUnknownReason(getMechanismName(), t);
}
}
}