Thursday, 8 February 2018

WildFly Elytron - Implementing a Custom HTTP Authentication Mechanism

When WildFly Elytron is used to secure a web application is is possible to implement custom HTTP authentication mechanisms that can be registered using the Elytron subsystem, it is then possible to override the configuration within the deployment to make use of this mechanism without requiring modifications to the deployment.

This blog post explores a custom authentication mechanism that can make use of custom HTTP headers to receive a clear text username and password and use these for authentication.  Generally passing clear text passwords is something that should be voided, they are only used here to avoid over complicating the example.

This blog will be making use of two projects within the elytron-examples respository that can be found at: -

https://github.com/wildfly-security-incubator/elytron-examples


  • simple-webapp - A very simple secured web application that can be deployed to test the mechanism.
  • simple-http-mechanism - A small project containing the minimal pieces to develop a custom mechanism.

Mechanism Implementation

The major piece of any custom HTTP mechanism is going to be the actual implementation of the mechanism, all custom mechanisms are required to implement the interface HttpServerAuthenticationMechanism.html.

In general for a mechanism the evaluateRequest method is called to handle the request passing in a HttpServerRequest object, the mechanism processes the requests and uses one of the following callback methods on the request to indicate the outcome: -
For each of these callback methods it is possible to pass in an instance of a HttpServerMechanismResponder which can be used to send challenge information to the calling client, which responders are called will depend very much on the outcome of the other mechanisms in use concurrently.


The custom mechanism implemented for this blog can be found at CustomHeaderHttpAuthenticationMechanism.java

Where this custom mechanism sends a challenge the challenge is always the same so we can use a static instance of the responder to avoid creating a new one each time we need to challenge: -

    private static final HttpServerMechanismsResponder RESPONDER = new      
            HttpServerMechanismsResponder() {       
        public void sendResponse(HttpServerResponse response) throws 
                HttpAuthenticationException {
            response.addResponseHeader(MESSAGE_HEADER, "Please resubit the request with a username specified using the X-USERNAME and a password specified using the X-PASSWORD header.");
            response.setStatusCode(401);
        }
    };


The evaluateRequest method then follows a path that will be common to many mechanisms.


final String username = request.getFirstRequestHeaderValue(USERNAME_HEADER);
final String password = request.getFirstRequestHeaderValue(PASSWORD_HEADER);

if (username == null || username.length() == 0 || password == null || 
      password.length() == 0) {    
    request.noAuthenticationInProgress(RESPONDER);
    return;
}


In this first block the mechanism tests if it has authentication headers appropriate for this mechanism, if not 'noAuthenticationInProgress' is called to notify the framework this mechanism is not doing anything yet and passes the responder in case a response is needed to challenge the client.


NameCallback nameCallback = new NameCallback("Remote Authentication Name", 
                                              username);
nameCallback.setName(username);
final PasswordGuessEvidence evidence = new 
                PasswordGuessEvidence(password.toCharArray());
EvidenceVerifyCallback evidenceVerifyCallback = new 
                EvidenceVerifyCallback(evidence);
try {
    callbackHandler.handle(new Callback[] { nameCallback, 
                                            evidenceVerifyCallback });
} catch (IOException | UnsupportedCallbackException e) {
    throw new HttpAuthenticationException(e);
}

if (evidenceVerifyCallback.isVerified() == false) {
    request.authenticationFailed("Username / Password Validation Failed", 
                                 RESPONDER);
}


In this second block the mechanism takes the headers received from the client and uses them to perform authentication by passing Callbacks to the provided CallbackHandler.

After a successful authentication it is possible to associate any credentials received from the client with the resulting identity, this step is optional but can be useful.


try {
    callbackHandler.handle(new Callback[] {new IdentityCredentialCallback(
        new PasswordCredential(ClearPassword.createRaw(
                ClearPassword.ALGORITHM_CLEAR, 
                password.toCharArray())), true)});
} catch (IOException | UnsupportedCallbackException e) {
    throw new HttpAuthenticationException(e);
}

At this point the identity has been authenticated and the credential associated but no check has been performed to ensure the identity is allowed to login so the next step is an authorization.

try {
    callbackHandler.handle(new Callback[] {authorizeCallback});

    if (authorizeCallback.isAuthorized()) {
        callbackHandler.handle(new Callback[] { 
            AuthenticationCompleteCallback.SUCCEEDED });
        request.authenticationComplete();
    } else {
        callbackHandler.handle(new Callback[] { 
            AuthenticationCompleteCallback.FAILED });
        request.authenticationFailed("Authorization check failed.", RESPONDER);
    }
    return;
} catch (IOException | UnsupportedCallbackException e) {
    throw new HttpAuthenticationException(e);
}

In this last block finally an AuthenticationCompleteCallback is needed to indicate the overall outcome of the authentication as decided by the mechanism, the reason the mechanism makes this decision is because a mechanism could take into account additional information in addition beyond the outcomes from the callbacks - an an example the HTTP Digest information will asses the validity of the nonce late in the authentication process.

Mechanism Factory Implementation

After the mechanism implementation the next class required is a factory to return instances of the mechanism, factories implement the HttpAuthenticationFactory interface, in this example the factory only returns a single mechanism however a single factory could support multiple mechanisms.

Within the test project this is implemented in CustomMechanismFactory.java

The most important step within the factory is to double check the name of the mechanism requested, it is important for the factory to return null if it can not create the required mechanism.  The mechanism factory can also take into account properties in the Map passed in to decide if it can create the requested mechanism.

Advertising Availability

There are two different approaches that can be used to advertise the availability of a mechanism factory, the first is to implement a java.security.Provider with the HttpAuthenticationFactory registered as an available service once for each mechanism it supports.

This example however is very simple so instead of implementing a provider we use a java.util.ServiceLoader to discover the factory instead, to achieve this we add the descriptor org.wildfly.security.http.HttpServerAuthenticationMechanismFactory under META-INF/services the only contents required in this file are the fully qualified class name of the factory.

Mechanism Installation

At this stage the mechanism project can be built and installed in the application server as a module ready to be used, the project is a simple maven project so can be built with: -

    mvn clean install

For the next stage the application server does not need to be running, the following command can be executed within the jboss-cli to add the module to the application server: -

module add --name=org.wildfly.security.examples.custom-http \
 --resources=/path/elytron-examples/simple-http-mechanism/target/simple-http-mechanism-1.0.0.Alpha1-SNAPSHOT.jar \
 --dependencies=org.wildfly.security.elytron,javax.api

The required dependencies are very simple, the installed module just requires a dependency on the public Elytron API and the javax API for access to some of the common callbacks and related exceptions.

Testing - BASIC authentication

When testing changes within the application server it is often better to start with small changes and verify those are working before moving onto the next set of changes, often users that make a large number of changes at once find it difficult to track down at which stage an error was introduced.

The next step within this blog is to deploy the simple-webapp also in the elytron-examples repository and verify it works with HTTP BASIC authentication, once verified we will switch the configuration to use the new authentication mechanism.

Before starting the application server a new test account can be added using the add-user utility.

./add-user.sh -a -u testuser -p password -g Users

Now the application server can be started and the CLI connected to the application server.

When the sample application is deployed it will by default use the 'other' security domain so a mapping needs to be added to map this to an Elytron HTTP authentication factory: -

./subsystem=undertow/application-security-domain=other:add(
http-authentication-factory=application-http-authentication)

The simple-webapp can now be deployed directly using maven.

mvn wildfly:deploy

This application could be tested using a web browser as it is using a standard mechanism the browser would understand, however once we switch to using a custom mechanism the browser will not understand so it is better to test the call using a client that will allow us to manipulate the headers ourselves.

curl -v http://localhost:8080/simple-webapp/secured -u testuser:password

If everything is working output similar to the following should be seen (Some headers have been removed to make the output easier to read)

< HTTP/1.1 200 OK
...<
<html>
 <head><title>Secured Servlet</title></head>
 <body>
   <h1>Secured Servlet</h1>
   <p>
Current Principal 'testuser'    </p>
 </body>
</html>

At this stage authentication is being successfully applied to the web application backed by WildFly Elytron using the Elytron implementation of the HTTP BASIC authentication mechanism, the next step is to switch to using the custom mechanism.

Testing - Custom Mechanism

The first resource to add in the CLI is to discover the factory implementation.

./subsystem=elytron/service-loader-http-server-mechanism-factory=
custom-factory:add(module=org.wildfly.security.examples.custom-http)

After this is added a CLI command can be executed to immediately check which mechanisms the factory can create.

./subsystem=elytron/service-loader-http-server-mechanism-factory=
custom-factory:read-resource(include-runtime=true)
{
   "outcome" => "success",
   "result" => {
       "available-mechanisms" => ["CUSTOM_MECHANISM"],
       "module" => "org.wildfly.security.examples.custom-http"
   }
}
The next resource to add is a http-authentication-factory to tie the mechanism factory to a security-domain that will be used for the actual authentication.

./subsystem=elytron/http-authentication-factory=custom-mechanism:
add(http-server-mechanism-factory=custom-factory,
security-domain=ApplicationDomain,
mechanism-configurations=[{mechanism-name=CUSTOM_MECHANISM}])

The application-security-domain resource we added previously can now be updated to use this new http-authentication-factory.

./subsystem=undertow/application-security-domain=other:
write-attribute(name=http-authentication-factory, value=custom-mechanism)
./subsystem=undertow/application-security-domain=other:
write-attribute(name=override-deployment-config, value=true)

The second of those commands is important, the application is defined to use the BASIC authentication mechanism only, by overriding the deployment config the mechanisms from the http-authentication-factory will be used instead.

The server can now be reloaded.

:reload

The same curl command can be executed again but this time it is expected it will fail with output similar to the following.

curl -v http://localhost:8080/simple-webapp/secured -u testuser:password
< HTTP/1.1 401 Unauthorized
...
< X-MESSAGE: Please resubit the request with a username specified using the X-USERNAME and a password specified using the X-PASSWORD header.
...

Here the authentication mechanism has rejected the call and subsequently added a header describing how to authentication.

The curl command can now be modified to: -

curl -v http://localhost:8080/simple-webapp/secured -H "X-USERNAME:testuser" -H "X-PASSWORD:password"
< HTTP/1.1 200 OK
...
<
<html>
 <head><title>Secured Servlet</title></head>
 <body>
   <h1>Secured Servlet</h1>
   <p>
Current Principal 'testuser'    </p>
 </body>
</html>

The resulting output now shows authentication was successful again and the authenticated principal is 'testuser'.







No comments:

Post a Comment