HTTP Authentication with Elytron

Version 3

    HTTP Authentication with Elytron

     

    General Framework

     

    The Elytron HTTP authentication framework is built around the ability for multiple authentication mechanisms to be associate with a single context and during authentication multiple mechanisms working together to collaborate on performing the authentication, this is achieved by separating the validation phases of authentication from the challenging phases of authentication.

     

    As a HTTP request is received each mechanism is given the opportunity to evaluate the incoming request, the mechanism will have access to various pieces of information about the request such as HTTP headers, cookies, request parameters, the request stream or even scopes acting as containers to hold previously identified information.  Once evaluation is complete the mechanism can then report the status of the evaluation which could be as simple as reporting no authentication was attempted to reporting that authentication has started or even authentication has completed - in addition to the notification the mechanism can register a responder which will be responsible for sending any subsequent challenge to the client depending on what is appropriate for the mechanism.

     

    The framework operates on the premise that the first mechanism to successfully authenticate a request wins.  If a mechanism successfully authenticated the request and registered a responder this will be immediately called to turn the request around.  If no mechanisms succeeded but one reported authentication was in progress or if the resource being accessed mandates authentication then the registered responders will be called to send the appropriate challenges to the client.

     

     

    Mechanism SPI / API

     

    The interface to be implemented by authentication mechanisms is relatively simple HttpServerAuthenticationMechanism (WildFly Elytron 1.1.0.Beta7 API): -

     

    public interface HttpServerAuthenticationMechanism {
        String getMechanismName();
        void evaluateRequest(HttpServerRequest request) throws HttpAuthenticationException;
        default Object getNegotiatedProperty(String propertyName) {
            return null;
        }
        default <T> T getNegotiationProperty(String propertyName, Class<T> type) {
            Object property = getNegotiatedProperty(propertyName);
            if (property != null && type.isInstance(property)) {
                return type.cast(property);
            }
            return null;
        }
        default void dispose() {
        };
    }
    
    

     

    HttpServerRequest

     

    During request evaluation the mechanism has access to the HttpServerRequest (WildFly Elytron 1.1.0.Beta7 API) which provides access to three main areas: -

     

    General Request Information

     

        List<String> getRequestHeaderValues(final String headerName);
        String getFirstRequestHeaderValue(final String headerName);
        SSLSession getSSLSession();
        String getRequestMethod();
        URI getRequestURI();
        Map<String, List<String>> getParameters();
        Set<String> getParameterNames();
        List<String> getParameterValues(String name);
        String getFirstParameterValue(String name);
        List<HttpServerCookie> getCookies();
        InputStream getInputStream();
        InetSocketAddress getSourceAddress();
        boolean suspendRequest();
        boolean resumeRequest();
    

    With the exception of the last two these methods all provide access to general information about the current HTTP request, the final two methods are a special case so that the mechanism can ask for the current request to be suspended so it can subsequently be resumed, these are used by mechanisms that require additional round trips to the client not automatically handled by a web browser such as redirects.

    HttpScopes

    A HttpScope (WildFly Elytron 1.1.0.Beta7 API) is a context specific scope that provides the ability to handle attachments, provides access to resources and also notifications about the end of a scopes life.

     

    public interface HttpScope {
        default String getID() {
            return null;
        }
        default boolean supportsAttachments() {
            return false;
        }
        default void setAttachment(final String key, final Object value) {
            throw log.noAttachmentSupport();
        }
        default Object getAttachment(final String key) {
            throw log.noAttachmentSupport();
        }
        default <T> T getAttachment(final String key, final Class<T> type) {
            Object attachment = getAttachment(key);
            if (attachment != null && type.isInstance(attachment)) {
                return type.cast(attachment);
            }
            return null;
        }
        default boolean supportsInvalidation() {
            return false;
        }
        default boolean invalidate() {
            return false;
        }
        default boolean supportsResources() {
            return false;
        }
        default InputStream getResource(final String path) {
            return null;
        }
        default boolean supportsNotifications() {
            return false;
        }
        default void registerForNotification(Consumer<HttpServerScopes> notificationConsumer) {
        }
    }
    

     

    Although a scope is available there is no guarantee that all the methods will be supported so the appropriate 'supports' methods should be called by the authentication mechanism before the method is called.

     

    The different scopes available are represented by the Scope (WildFly Elytron 1.1.0.Beta7 API) enum,

     

    public enum Scope {
        APPLICATION,
        CONNECTION,
        EXCHANGE,
        GLOBAL,
        SESSION,
        SSL_SESSION;
    }
    

     

    On the HttpServerRequest the following methods are provided to access the scopes: -

     

        HttpScope getScope(Scope scope);
        Collection<String> getScopeIds(Scope scope);
        HttpScope getScope(Scope scope, String id);
    

     

    If a scope of a specific type is not available the method will return null so it is important that mechanisms check a scope was returned to avoid NullPointerExceptions.

     

    Outcome Notifications

    The final set of methods on the HttpServerRequest method allow for the mechanism to indicate the outcome of the evaluation attempt and also register a responder to allow for a subsequent challenge or response to be sent to the client.

     

        void noAuthenticationInProgress(final HttpServerMechanismsResponder responder);
        default void noAuthenticationInProgress() {
            noAuthenticationInProgress(null);
        }
        void authenticationInProgress(final HttpServerMechanismsResponder responder);
        void authenticationComplete(final HttpServerMechanismsResponder responder);
        default void authenticationComplete() {
            authenticationComplete(null);
        }
        void authenticationFailed(final String message, final HttpServerMechanismsResponder responder);
        default void authenticationFailed(final String message) {
            authenticationFailed(message, null);
        }
        void badRequest(HttpAuthenticationException failure, final HttpServerMechanismsResponder responder);
        default void badRequest(HttpAuthenticationException failure) {
            badRequest(failure, null);
        }
    

     

    The APIs deliberately split access to the request from access to the response to ensure the correct split in the processing and ensure a request evaluation phase and and response phase. 

     

    When a mechanism registers a responder it is not guaranteed that the responder is called as other checks and decisions are made to decide which ones to call, as an example one mechanism could register it's responder for the subsequent mechanism to succeed at authentation.  Mechanisms however can optionally implement the dispose method to be notified that the current request processing is ending.

     

    HttpServerMechanismResponder

     

    The HttpServerMechanismsResponder (WildFly Elytron 1.1.0.Beta7 API) is a simple interface implemented by the authentication mechanism to handle sending a response.

     

    @FunctionalInterface
    public interface HttpServerMechanismsResponder {
        void sendResponse(HttpServerResponse response) throws HttpAuthenticationException;
    }
    

     

    When it is called the HttpServerMechanismResponder is provided a HttpServerResponse (WildFly Elytron 1.1.0.Beta7 API) to allow it to manipulate the response being sent.  As a mechanism is responsible for setting the responder during the evaluateRequest method it is possible for the mechanism to access methods on the HttpServerRequest object other than to attempt to adjust the outcome of the authentication attempt.

     

    HttpServerResponse

     

    The methods available on the HttpServerResponse is a relatively small set of methods to provide access to parts of the response a mechanism is expected to be required to access.

    public interface HttpServerResponse {
        void addResponseHeader(final String headerName, final String headerValue);
        void setStatusCode(final int statusCode);
        void setResponseCookie(final HttpServerCookie cookie);
        OutputStream getOutputStream();
        boolean forward(String path);
    }
    

     

    One point to be aware of here is that responders from multiple mechanisms could be called, each of those could attempt to set the status code - as a HTTP response can only contain a single status code if multiple status codes are set the first non-200 status code will be used. 

     

    Mechanism Discovery

     

    The authentication mechanisms are instantiated by factory implementations in very much the same Sasl authentication mechanisms are discovered and instantiated.

     

    SaslServerFactory (Java Platform SE 8 )

     

    public interface HttpServerAuthenticationMechanismFactory {
        String[] getMechanismNames(Map<String, ?> properties);
        HttpServerAuthenticationMechanism createAuthenticationMechanism(String mechanismName, Map<String, ?> properties, CallbackHandler callbackHandler) throws HttpAuthenticationException;
    }
    
    
    
    

    HttpServerAuthenticationMechanismFactory (WildFly Elytron 1.1.0.Beta5 API)

     

    These factories can be discovered by Elytron either by having them registered with a java.security.Provider with a service type of 'HttpServerAuthenticationMechanismFactory' or by including the appropriate META-INF/services configuration so they can be discovered using a java.util.ServiceLoader.

     

    HTTP Authentication Factory and Mechanism Configuration

    When the authentication mechanisms are used by the server they are brought together with a HttpAuthenticationFactory (WildFly Elytron 1.1.0.Beta7 API)  this takes an HttpServerAuthenticationMechanismFactory, a SecurityDomain (WildFly Elytron 1.1.0.Beta7 API), and a  MechanismConfigurationSelector (WildFly Elytron 1.1.0.Beta7 API).

     

    As described above the HttpServerAuthenticationMechanismFactory is used to instantiate the authentication mechanisms, the SecurityDomain is used to obtain the CallbackHandler for use by the mechanism and in the event of a successful authentication will be the source of the SecurityIdentity (WildFly Elytron 1.1.0.Beta7 API), the MechanismConfigurationSelector is responsible for providing MechanismConfiguration (WildFly Elytron 1.1.0.Beta7 API) based on information in the MechanismInformation (WildFly Elytron 1.1.0.Beta7 API).

     

    Server Integration SPI

    The Elytron HTTP authentication framework is independent of the server actually handling the requests, an SPI is provided to enable integration HttpExchangeSpi (WildFly Elytron 1.1.0.Beta7 API): -

     

    public interface HttpExchangeSpi {
        List<String> getRequestHeaderValues(final String headerName);
        void addResponseHeader(final String headerName, final String headerValue);
        default String getFirstRequestHeaderValue(final String headerName) {
            List<String> headerValues = getRequestHeaderValues(headerName);
            return headerValues != null && headerValues.size() > 0 ? headerValues.get(0) : null;
        }
        default SSLSession getSSLSession() {
            return null;
        }
        default HttpScope getScope(Scope scope) {
            return null;
        }
        default Collection<String> getScopeIds(Scope scope) {
            return null;
        }
        default HttpScope getScope(Scope scope, String id) {
            return null;
        }
        void setStatusCode(final int statusCode);
        void authenticationComplete(final SecurityIdentity securityIdentity, final String mechanismName);
        void authenticationFailed(final String message, final String mechanismName);
        void badRequest(final HttpAuthenticationException error, final String mechanismName);
        String getRequestMethod();
        URI getRequestURI();
        Map<String, List<String>> getRequestParameters();
        default Set<String> getRequestParameterNames() {
            return getRequestParameters().keySet();
        }
        default List<String> getRequestParameterValues(String name) {
            Map<String, List<String>> parameters = getRequestParameters();
            if (parameters != null) {
                return parameters.get(name);
            }
    
    
            return null;
        }
        default String getFirstRequestParameterValue(String name) {
            List<String> values = getRequestParameterValues(name);
            if (values != null && values.size() > 0) {
                return values.get(0);
            }
    
    
            return null;
        }
        List<HttpServerCookie> getCookies();
        InputStream getRequestInputStream();
        InetSocketAddress getSourceAddress();
        void setResponseCookie(HttpServerCookie cookie);
        OutputStream getResponseOutputStream();
         default int forward(String path) {
             return -1;
         }
         default boolean suspendRequest() {
             return false;
         }
         default boolean resumeRequest() {
             return false;
         }
    }
    
    
    

     

    Unlike the APIs used by the mechanisms this SPI combined both request and response handling into a single interface, the javadoc contains more information but many of the methods are allowed to return 'null' if a specific integration feature is not supported.

     

    Whilst a number of the integration points in this API are optional it does need to be noted that certain authentication mechanisms may depend on those features being available so those mechanisms may become incompatible with the server the mechanism is running on.  As an example the FORM authentication mechanism requires access to a HttpScope so that the client is not prompted on every request to authenticate.

     

    Undertow Integration Details

    Integration with Undertow is mainly achieved in an independent project / module GitHub - wildfly-security/elytron-web: Integration project for integrating Elytron based HTTP authentication with web co… - the reason for this separation is so that Elytron does not need to depend directly on Undertow and also so that Undertow does not need to depend directly on Elytron.

     

    The implementation of HttpExchangeSpi is predominantly a 1:1 mapping between Undertow equivalent methods https://github.com/wildfly-security/elytron-web/blob/master/undertow/src/main/java/org/wildfly/elytron/web/undertow/server/ElytronHttpExchange.java in addition to this alternative HttpScope resolvers can be registered - this is used when running within the application server to provide access to scopes backed by Servlet defined components.

     

    Included Mechanisms

    The Elytron project contains default implementations of the following authentication mechanisms: -

    • BASIC
    • CLIENT_CERT
    • FORM
    • SPNEGO