Thursday, 18 October 2018

Using WildFly Elytron JASPI with Standalone Undertow

As part of the development efforts for WildFly 15 an implementation of the servlet profile from the JASPI (JSR-196) specification has been added to WildFly Elytron and is in the final stages of being integrated within the application server.

As with many of the features included in WildFly Elytron it is possible to make use of this outside of the application server, this blog post combined with an example project illustrates how the WildFly Elytron JASPI implementation can be used with a standalone Undertow server.

The example project can be found at the following location: -

https://github.com/wildfly-security-incubator/elytron-examples/tree/master/undertow-standalone-jaspi

The project can be built using Apache Maven using the following command: -

mvn clean install

And executed with: -

mvn exec:exec

The remainder of this blog post will describe the remainder of the project and then demonstrate how to make an invocation to the servlet using 'curl'.

Server Auth Module

This project contains a very simple ServerAuthModule 'org.wildfly.security.examples.jaspi.SimpleServerAuthModule', this module expects to receive a username using the 'X-USERNAME' header and expects to receive a password using the 'X-PASSWORD' header.

This module does not perform it's own username and password validation, instead it makes use of a 'PasswordValidationCallback' which is handled by the 'CallbackHandler' passed to the module on initialisation, this means the verification is in turn handled by WildFly Elytron.

Finally this module makes use of a 'GroupPrincipalCallback' which is used to override the groups / role assigned to the resulting identity and in this case assign the identity the 'Users' role.

Although this example demonstrates delegating the verification of the username and password to WildFly Elytron it is also possible to implement a ServerAuthModule which performs it's own validation and instead just describe the resulting identity using the callbacks.

Servlet

To make this demonstration possible the example project contains a servlet 'org.wildfly.security.examples.servlet.SecuredServlet', the purpose of this servlet is to return a HTML page containing the name of the current authenticated identity.

Configuration and Execution

Undertow and WildFly Elytron are programatically configured and started within 'org.wildfly.security.examples.HelloWorld', this class contains some initialisation that has been seen previously and some new configuration to enable JASPI and integrate with Undertow.

createSecurityDomain()

The first step is to create the SecurityDomain backed by a SecurityRealm: -

private static SecurityDomain createSecurityDomain() throws Exception {
    PasswordFactory passwordFactory = PasswordFactory.getInstance(ALGORITHM_CLEAR, elytronProvider);

    Map<String, SimpleRealmEntry> identityMap = new HashMap<>();
    identityMap.put("elytron", new SimpleRealmEntry(Collections.singletonList(
        new PasswordCredential(passwordFactory.generatePassword(new ClearPasswordSpec("Coleoptera".toCharArray()))))));

    SimpleMapBackedSecurityRealm simpleRealm = new SimpleMapBackedSecurityRealm(() -> new Provider[] { elytronProvider });
    simpleRealm.setIdentityMap(identityMap);

    SecurityDomain.Builder builder = SecurityDomain.builder()
            .setDefaultRealmName("TestRealm");

    builder.addRealm("TestRealm", simpleRealm).build();
    builder.setRoleMapper(RoleMapper.constant(Roles.of("Test")));

    builder.setPermissionMapper((principal, roles) -> PermissionVerifier.from(new LoginPermission()));

    return builder.build();
}

Here we have a single identity 'elytron' with the password 'Coleoptera', however any of the other security realms could have been used here allowing for alternative integration options.

configureJaspi()

The next step is the JASPI configuration, at this stage this is still independent of the Undertow initialisation: -

private static String configureJaspi() {
    AuthConfigFactory authConfigFactory = new ElytronAuthConfigFactory();
    AuthConfigFactory.setFactory(authConfigFactory);

    return JaspiConfigurationBuilder.builder(null, null)
            .setDescription("Default Catch All Configuration")
            .addAuthModuleFactory(SimpleServerAuthModule::new)
            .register(authConfigFactory);
}


The first step is to initialise the Elytron implementation of 'AuthConfigFactory' and to register it as the global default implementation.

The JASPI APIs provide a lot of flexibility to allow for programatic registration of configurations, however the APIs do not provide a way to instantiate the implementations of these implementations when in the majority of the cases where dynamic registration is used it is just to register a custom ServerAuthModule.  With the changes added to Wildfly Elytron we have added a new API of our own in a class called 'org.wildfly.security.auth.jaspi.JaspiConfigurationBuilder' - this can be used to register a configuration with the 'AuthConfigFactory'.

Configuring and Starting Undertow

Now that the prior two steps have been completed it is possible to use the Undertow and WildFly Elytron APIs to complete the configuration of a deployment and start the Undertow server with JASPI authentication enabled.

The first step is using the Undertow APIs to define a deployment: -

DeploymentInfo deploymentInfo = Servlets.deployment()
        .setClassLoader(SecuredServlet.class.getClassLoader())
        .setContextPath(PATH)
        .setDeploymentName("helloworld.war")
        .addSecurityConstraint(new SecurityConstraint()
                .addWebResourceCollection(new WebResourceCollection()
                        .addUrlPattern("/secured/*"))
                .addRoleAllowed("Users")
                .setEmptyRoleSemantic(SecurityInfo.EmptyRoleSemantic.DENY))
        .addServlets(Servlets.servlet(SecuredServlet.class)
                .addMapping(SERVLET));

Next we can use the WildFly Elytron APIs to apply Wildfly Elytron backed security to this deployment: -

AuthenticationManager authManager = AuthenticationManager.builder()
        .setSecurityDomain(securityDomain)
        .build();
authManager.configure(deploymentInfo);

It is worth noting this is where the WildFly Elytron SecurityDomain is associated with the deployment, the JASPI configuration performed earlier was independent of the domain.

The final stages are now to complete the deployment before creating and starting the Undertow server: -

DeploymentManager deployManager = Servlets.defaultContainer().addDeployment(deploymentInfo);
deployManager.deploy();

PathHandler path = Handlers.path(Handlers.redirect(PATH))
        .addPrefixPath(PATH, deployManager.start());

Undertow server = Undertow.builder()
        .addHttpListener(PORT, HOST)
        .setHandler(path)
        .build();
server.start();

Invoking the Servlet

Once the server is running it is now time to invoke the servlet using 'curl', other clients could also be used however they would need to support custom headers to work with this mechanism.

Firstly if we call the servlet without any headers we should see the request rejected with a message asking for the headers: -

]$ curl -v http://localhost:28080/helloworld/secured
...
< HTTP/1.1 401 Unauthorized
< Expires: 0
< Connection: keep-alive
< Cache-Control: no-cache, no-store, must-revalidate
< Pragma: no-cache
< X-MESSAGE: Please resubmit the request with a username specified using the X-USERNAME and a password specified using the X-PASSWORD header.
< Content-Type: text/html;charset=UTF-8
< Content-Length: 71

We can now repeat the command and provide a username and password using the appropriate headers: -

]$ curl -v http://localhost:28080/helloworld/secured -H "X-Username:elytron" -H "X-Password:Coleoptera"
...
< HTTP/1.1 200 OK
< Expires: 0
< Connection: keep-alive
< Cache-Control: no-cache, no-store, must-revalidate
< Pragma: no-cache
< Content-Length: 154
<html>
  <head><title>Secured Servlet</title></head>
  <body>
    <h1>Secured Servlet</h1>
    <p>
 Current Principal 'elytron'    </p>
  </body>
</html>

In this case we now see the expected HTML specifying the name of the current identity.