/* * JBoss, Home of Professional Open Source. * Copyright 2015 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; import static org.wildfly.security._private.ElytronMessages.log; import static org.wildfly.security.http.HttpConstants.FORBIDDEN; import static org.wildfly.security.http.HttpConstants.OK; import static org.wildfly.security.http.HttpConstants.SECURITY_IDENTITY; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.URI; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; import javax.net.ssl.SSLSession; import org.wildfly.common.Assert; import org.wildfly.security.auth.server.SecurityDomain; import org.wildfly.security.auth.server.SecurityIdentity; /** * A HTTP based authenticator responsible for performing the authentication of the current request based on the policies of the * associated {@link SecurityDomain}. * * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a> */ public class HttpAuthenticator { private final Supplier<List<HttpServerAuthenticationMechanism>> mechanismSupplier; private final HttpExchangeSpi httpExchangeSpi; private final boolean required; private final boolean ignoreOptionalFailures; private final Consumer<Runnable> logoutHandlerConsumer; private volatile boolean authenticated = false; private HttpAuthenticator(final Supplier<List<HttpServerAuthenticationMechanism>> mechanismSupplier, final HttpExchangeSpi httpExchangeSpi, final boolean required, final boolean ignoreOptionalFailures, Consumer<Runnable> logoutHandlerConsumer) { this.mechanismSupplier = mechanismSupplier; this.httpExchangeSpi = httpExchangeSpi; this.required = required; this.ignoreOptionalFailures = ignoreOptionalFailures; this.logoutHandlerConsumer = logoutHandlerConsumer; } /** * Perform authentication for the request. * * @return {@code true} if the call should be allowed to continue within the web server, {@code false} if the call should be * returning to the client. * @throws HttpAuthenticationException */ public boolean authenticate() throws HttpAuthenticationException { return new AuthenticationExchange().authenticate(); } private boolean isAuthenticated() { return authenticated; } /** * Construct and return a new {@code Builder} to configure and create an instance of {@code HttpAuthenticator}. * * @return a new {@code Builder} to configure and create an instance of {@code HttpAuthenticator}. */ public static Builder builder() { return new Builder(); } private class AuthenticationExchange implements HttpServerRequest, HttpServerResponse { private volatile HttpServerAuthenticationMechanism currentMechanism; private volatile boolean authenticationAttempted = false; private volatile int statusCode = -1; private volatile boolean statusCodeAllowed = false; private volatile List<HttpServerMechanismsResponder> responders; private volatile HttpServerMechanismsResponder successResponder; private boolean authenticate() throws HttpAuthenticationException { List<HttpServerAuthenticationMechanism> authenticationMechanisms = mechanismSupplier.get(); if (required && authenticationMechanisms.size() == 0) { throw log.httpAuthenticationNoMechanisms(); } responders = new ArrayList<>(authenticationMechanisms.size()); boolean evaluationFailed = false; try { for (HttpServerAuthenticationMechanism nextMechanism : authenticationMechanisms) { currentMechanism = nextMechanism; try { nextMechanism.evaluateRequest(this); } catch (HttpAuthenticationException e) { // Give all mechanisms an opportunity to succeed, where a mechanism fails due to mis-configuration or a transient error // others may still be able to operate correctly. evaluationFailed = true; log.trace("Request evaluation for mechanism '%s' failed.", nextMechanism.getMechanismName(), e); } if (isAuthenticated()) { if (successResponder != null) { statusCodeAllowed = true; successResponder.sendResponse(this); if (statusCode > 0) { httpExchangeSpi.setStatusCode(statusCode); return false; } } return true; } } currentMechanism = null; if (required || (authenticationAttempted && ignoreOptionalFailures == false)) { statusCodeAllowed = true; if (responders.size() > 0) { boolean atLeastOneChallenge = false; boolean statusSet = false; for (HttpServerMechanismsResponder responder : responders) { try { responder.sendResponse(this); atLeastOneChallenge = true; if (statusSet == false && statusCode > 0 && statusCode != OK) { httpExchangeSpi.setStatusCode(statusCode); statusSet = true; } } catch (HttpAuthenticationException e) { log.trace("HTTP authentication mechanism unable to send challenge.", e); } } if (atLeastOneChallenge == false) { throw log.httpAuthenticationNoSuccessfulResponder(); } if (statusSet == false) { httpExchangeSpi.setStatusCode(OK); } } else { if (evaluationFailed) { throw log.httpAuthenticationFailedEvaluatingRequest(); } httpExchangeSpi.setStatusCode(FORBIDDEN); } return false; } // If authentication was required it should have been picked up in the previous block. return true; } finally { authenticationMechanisms.forEach(m -> m.dispose()); } } @Override public List<String> getRequestHeaderValues(String headerName) { return httpExchangeSpi.getRequestHeaderValues(headerName); } @Override public String getFirstRequestHeaderValue(String headerName) { return httpExchangeSpi.getFirstRequestHeaderValue(headerName); } @Override public SSLSession getSSLSession() { return httpExchangeSpi.getSSLSession(); } @Override public Certificate[] getPeerCertificates() { return httpExchangeSpi.getPeerCertificates(required); } @Override public HttpScope getScope(Scope scope) { return httpExchangeSpi.getScope(scope); } @Override public Collection<String> getScopeIds(Scope scope) { return httpExchangeSpi.getScopeIds(scope); } @Override public HttpScope getScope(Scope scope, String id) { return httpExchangeSpi.getScope(scope, id); } @Override public void noAuthenticationInProgress(HttpServerMechanismsResponder responder) { if (responder != null) { responders.add(responder); } } @Override public void authenticationInProgress(HttpServerMechanismsResponder responder) { authenticationAttempted = true; if (responder != null) { responders.add(responder); } } @Override public void authenticationComplete(HttpServerMechanismsResponder responder) { authenticated = true; httpExchangeSpi.authenticationComplete( currentMechanism.getNegotiationProperty(SECURITY_IDENTITY, SecurityIdentity.class), currentMechanism.getMechanismName()); successResponder = responder; } @Override public void authenticationComplete(HttpServerMechanismsResponder responder, Runnable logoutHandler) { authenticationComplete(responder); if (logoutHandlerConsumer != null) { logoutHandlerConsumer.accept(logoutHandler); } } @Override public void authenticationFailed(String message, HttpServerMechanismsResponder responder) { authenticationAttempted = true; httpExchangeSpi.authenticationFailed(message, currentMechanism.getMechanismName()); if (responder != null) { responders.add(responder); } } @Override public void badRequest(HttpAuthenticationException failure, HttpServerMechanismsResponder responder) { authenticationAttempted = true; httpExchangeSpi.badRequest(failure, currentMechanism.getMechanismName()); if (responder != null) { responders.add(responder); } } @Override public String getRequestMethod() { return httpExchangeSpi.getRequestMethod(); } @Override public URI getRequestURI() { return httpExchangeSpi.getRequestURI(); } @Override public String getRequestPath() { return httpExchangeSpi.getRequestPath(); } @Override public Map<String, List<String>> getParameters() { return httpExchangeSpi.getRequestParameters(); } @Override public Set<String> getParameterNames() { return httpExchangeSpi.getRequestParameterNames(); } @Override public List<String> getParameterValues(String name) { return httpExchangeSpi.getRequestParameterValues(name); } @Override public String getFirstParameterValue(String name) { return httpExchangeSpi.getFirstRequestParameterValue(name); } @Override public List<HttpServerCookie> getCookies() { return httpExchangeSpi.getCookies(); } @Override public InputStream getInputStream() { return httpExchangeSpi.getRequestInputStream(); } @Override public InetSocketAddress getSourceAddress() { return httpExchangeSpi.getSourceAddress(); } @Override public void addResponseHeader(String headerName, String headerValue) { httpExchangeSpi.addResponseHeader(headerName, headerValue); } @Override public void setStatusCode(int statusCode) { if (statusCodeAllowed == false) { throw log.statusCodeNotNow(); } if (this.statusCode < 0 || statusCode != OK) { this.statusCode = statusCode; } } @Override public OutputStream getOutputStream() { return httpExchangeSpi.getResponseOutputStream(); } @Override public void setResponseCookie(HttpServerCookie cookie) { httpExchangeSpi.setResponseCookie(cookie); } @Override public boolean forward(String path) { int statusCode = httpExchangeSpi.forward(path); if (statusCode > 0) { setStatusCode(statusCode); return true; } return false; } @Override public boolean suspendRequest() { return httpExchangeSpi.suspendRequest(); } @Override public boolean resumeRequest() { return httpExchangeSpi.resumeRequest(); } } /** * A {@code Builder} to configure and create an instance of {@code HttpAuthenticator}. */ public static class Builder { private Supplier<List<HttpServerAuthenticationMechanism>> mechanismSupplier; private HttpExchangeSpi httpExchangeSpi; private boolean required; private boolean ignoreOptionalFailures; private Consumer<Runnable> logoutHandlerConsumer; Builder() { } /** * Set the {@link Supplier<List<HttpServerAuthenticationMechanism>>} to use to obtain the actual {@link HttpServerAuthenticationMechanism} instances based * on the configured policy. * * @param mechanismSupplier the {@link Supplier<List<HttpServerAuthenticationMechanism>>} with the configured authentication policy. * @return the {@link Builder} to allow method call chaining. */ public Builder setMechanismSupplier(Supplier<List<HttpServerAuthenticationMechanism>> mechanismSupplier) { this.mechanismSupplier = mechanismSupplier; return this; } /** * Set the {@link HttpExchangeSpi} instance for the current request to allow integration with the Elytron APIs. * * @param httpExchangeSpi the {@link HttpExchangeSpi} instance for the current request * @return the {@link Builder} to allow method call chaining. */ public Builder setHttpExchangeSpi(final HttpExchangeSpi httpExchangeSpi) { this.httpExchangeSpi = httpExchangeSpi; return this; } /** * Sets if authentication is required for the current request, if not required mechanisms will be called to be given the * opportunity to authenticate * * @param required is authentication required for the current request? * @return the {@link Builder} to allow method call chaining. */ public Builder setRequired(final boolean required) { this.required = required; return this; } /** * Where authentication is not required but is still attempted a failure of an authentication mechanism will cause the * challenges to be sent and the current request returned to the client, setting this value to true will allow the * failure to be ignored. * * This setting has no effect when required is set to {@code true}, in that case all failures will result in a client * * @param ignoreOptionalFailures should mechanism failures be ignored if authentication is optional. * @return the {@link Builder} to allow method call chaining. */ public Builder setIgnoreOptionalFailures(final boolean ignoreOptionalFailures) { this.ignoreOptionalFailures = ignoreOptionalFailures; return this; } /** * <p>A {@link Consumer} responsible for registering a {@link Runnable} emitted by one of the mechanisms during the evaluation * of a request and representing some action to be taken during logout. * * <p>This method is mainly targeted for programmatic logout where logout requests are send by the application after the * authentication. Although, integration code is free to register the logout handler whatever they want in order to support * different logout scenarios. * * @param logoutHandlerConsumer the consumer responsible for registering the logout handler (not {@code null}) * @return the {@link Builder} to allow method call chaining. */ public Builder registerLogoutHandler(Consumer<Runnable> logoutHandlerConsumer) { this.logoutHandlerConsumer = Assert.checkNotNullParam("logoutHandlerConsumer", logoutHandlerConsumer); return this; } /** * Build the new {@code HttpAuthenticator} instance. * * @return the new {@code HttpAuthenticator} instance. */ public HttpAuthenticator build() { return new HttpAuthenticator(mechanismSupplier, httpExchangeSpi, required, ignoreOptionalFailures, logoutHandlerConsumer); } } }