How to configure an Elytron JAAS Security Realm

This article is a walk though the configuration of an Elytron JAAS security Realm on WildFly application server. We will shortly review how JAAS configuration works and then we will deploy an example application which leverages the JAAS Security Configuration file.

JAAS in a nutshell

The Java Authentication and Authorization Service (JAAS) is a security framework for Java applications which uses pluggable authentication modules (PAM). JAAS provides subject-based authorization on authenticated identities and it is integrated into the Java Runtime Environment (JRE).

Java Authentication and Authorization Service (JAAS) with wildfly

Securing applications with JAAS is a two-step process which includes:

  • Coding a LoginModule by extending the Interface javax.security.auth.spi.LoginModule
  • Adding a login configuration file, which consists of one or more entries, each specifying which underlying authentication technology should be used for a particular application or applications

The structure of the login configuration file follows here:

<entry name> { 
    <LoginModule> <flag> <options>;   
    . . .
};

As an example, here is a sample configuration file which leverages the Krb5LoginModule that authenticates users using Kerberos protocols:

KerberosAuthentication {
	com.sun.security.auth.module.Krb5LoginModule required
		debug="true"
		storeKey="true"
		useTicketCache="false";
};

Configuring an Elytron JAAS Realm

  • Hard requirements: WildFly 26+ or JBoss EAP 8

As next step, we will create a simple LoginModule which uses a static map of usernames/passwords to determine if the Login is successful or not. (Check at the end of the tutorial for the source code of this project).

Here is the CustomLoginModule:

public class CustomLoginModule implements LoginModule {

    private final Map<String, char[]> usersMap = new HashMap<String, char[]>();
    private Principal principal;
    private Subject subject;
    private CallbackHandler handler;

    @Override
    public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
        this.subject = subject;
        this.handler = callbackHandler;
        this.usersMap.put("user1", "password1".toCharArray());
        this.usersMap.put("user2", "password2".toCharArray());
    }

    @Override
    public boolean login() throws LoginException {
        // obtain the incoming username and password from the callback handler
        NameCallback nameCallback = new NameCallback("Username");
        PasswordCallback passwordCallback = new PasswordCallback("Password", false);
        Callback[] callbacks = new Callback[]{nameCallback, passwordCallback};
        try {
            this.handler.handle(callbacks);
        } catch (UnsupportedCallbackException | IOException e) {
            throw new LoginException("Error handling callback: " + e.getMessage());
        }

        final String username = nameCallback.getName();
        this.principal = new NamePrincipal(username);
        final char[] password = passwordCallback.getPassword();

        char[] storedPassword = this.usersMap.get(username);
        if (!Arrays.equals(storedPassword, password)) {
            throw new LoginException("Invalid password");
        } else {
            return true;
        }
    }

    @Override
    public boolean commit() throws LoginException {
        if (this.principal.getName().equals("user1") || this.principal.getName().equals("user2")) {
            this.subject.getPrincipals().add(new Roles("Admin"));
            this.subject.getPrincipals().add(new Roles("User"));
            this.subject.getPrincipals().add(new Roles("Guest"));
        }
        return true;
    }

    @Override
    public boolean abort() throws LoginException {
        return true;
    }

    @Override
    public boolean logout() throws LoginException {
        this.subject.getPrincipals().clear();
        return true;
    }

    private static class Roles implements Principal {

        private final String name;

        Roles(final String name) {
            this.name = name;
        }

        public String getName() {
            return this.name;
        }
    }
}

As you can see, the LoginModule interface specifies five abstract methods that require implementations::

  • initialize: initializes the LoginModule with the relevant authentication and state information.
  • login: authenticates a Subject. This is phase 1 of authentication and can involve a call to the CallbackHandler handle method if user interaction is required.
  • commit: commits the authentication process. This is phase 2 of authentication when phase 1 succeeds.
  • abort: aborts the authentication process. This is phase 2 of authentication when phase 1 fails.
  • logout: logs out a Subject.

The CallBackHandler which we have registered in the login method follows here:

public class CustomCallbackHandler implements CallbackHandler {

    private Principal principal;
    private char[] evidence;

    public CustomCallbackHandler() {
    }


    public void setSecurityInfo(final Principal principal, final Object evidence) {
        this.principal = principal;
        if (evidence instanceof PasswordGuessEvidence) {
            this.evidence = ((PasswordGuessEvidence) evidence).getGuess();
        }
    }

    public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
        if (callbacks == null) {
            throw new IllegalArgumentException("The callbacks argument cannot be null");
        }

        for (Callback callback : callbacks) {
            if (callback instanceof NameCallback && principal != null) {
                NameCallback nameCallback = (NameCallback) callback;
                nameCallback.setName(this.principal.getName());
            } else if (callback instanceof PasswordCallback) {
                PasswordCallback passwordCallback = (PasswordCallback) callback;
                passwordCallback.setPassword((this.evidence));
            } else {
                throw new UnsupportedCallbackException(callback, "Unsupported callback");
            }
        }
    }
}

This CallBackHandler allows the LoginModule to communicate with the user (be it an individual or an external system or service) to collect authentication information (typically username and password) and, at the same time, ensure implementation independence.

Finally, the JAAS Configuration file (JAAS-login-modules.conf) follows here:

test {
    loginmodules.CustomLoginModule optional;

};

In the above configuration, we reference our CustomLoginModule and we set it as optional which means that it is not required to succeed. Whether or not it succeeds, authentication proceeds down the LoginModule list.

Building the Custom Module

At this point, you have the following project structure:

├── JAAS-login-modules.conf
├── pom.xml
├── src
│   └── main
│       └── java
│           └── loginmodules
│               ├── CustomCallbackHandler.java
│               └── CustomLoginModule.java

To compile and build the CustomModule we require the following dependencies:

<dependencies>
    <dependency>
        <groupId>org.wildfly.security</groupId>
        <artifactId>wildfly-elytron</artifactId>
        <version>1.17.2.Final</version>
    </dependency>
    <dependency>
        <groupId>javax.security.enterprise</groupId>
        <artifactId>javax.security.enterprise-api</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

Moreover, notice that we also need wildfly-elytron artifact as we reference the Class org.wildfly.security.auth.principal.NamePrincipal in our code.

Finally, build the Maven project so that you create a JAR file custom-login-modules-1.0.jar:

mvn install

Installing the Module

Firslty, we will install the Custom Login Module as a module of the application server and use it to configure a JAAS Elytron Realm.

The following CLI batch script starts by defining the path to the custom-login-modules-1.0.jar and then it creates the elytron Realm and Security Domain. In conclusion, the Security Domain is plugged in the ejb3 and undertow subsystem so that you can use it both for EJB and Web applications:

set projectpath=/home/francesco/git/mastertheboss/security/jaas-elytron

batch

module add --name=lm --resources=$projectpath/target/custom-login-modules-1.0.jar --dependencies=org.wildfly.security.elytron

/subsystem=elytron/jaas-realm=myRealm:add(entry=test,path=$projectpath/JAAS-login-modules.conf,module=lm,callback-handler=loginmodules.CustomCallbackHandler)

/subsystem=elytron/security-domain=mySD:add(default-realm=myRealm,realms=[{realm=myRealm}],permission-mapper=default-permission-mapper)

/subsystem=elytron/http-authentication-factory=example-loginconfig-http-auth:add(http-server-mechanism-factory="global",mechanism-configurations=[{mechanism-name="BASIC",mechanism-realm-configurations=[{realm-name="FSRealmUsers"}]}],security-domain=mySD)

/subsystem=ejb3/application-security-domain=other:write-attribute(name=security-domain,value=mySD)

/subsystem=undertow/application-security-domain=other:write-attribute(name=http-authentication-factory,value=example-loginconfig-http-auth)
/subsystem=undertow/application-security-domain=other:undefine-attribute(name=security-domain)

run-batch
reload

Next, run the above CLI script and check that it completes successfully.

/jboss-cli.sh --connect --file=configure.cli

Testing the JAAS Realm in a server application

To test our LoginModule, we can simply deploy an application which requires one Role we provide through the LoginModule. For example, the “Admin” Role. You can add the @RolesAllowed annotation to a REST Endpoint as an example:

@Path("ping")
@Stateless
public class Ping {

    @Resource
    private SessionContext sessionContext;

    @Resource
    private EJBContext ejbContext;

    @GET
    @RolesAllowed("Admin")
    public String ping() {
        return sessionContext == null ? "No session context! " :  "Hello " + sessionContext.getCallerPrincipal().getName() + "!";
    }

}

Finally, we need to reference the “other” security domain in our jboss-web.xml file as follows:

<jboss-web xmlns="http://www.jboss.com/xml/ns/javaee"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/schema/jbossas/jboss-web_13_0.xsd"
           version="13.0">
    <security-domain>other</security-domain>
</jboss-web>

Testing the application

Finally, you can test your application using the user1/password2 credentials which will give you “Admin” role:

jaas tutorial wildfly jboss

Source code

The source code for the JAAS module is available here: https://github.com/fmarchioni/mastertheboss/tree/master/security/jaas-elytron

This example is a fork from the jaas realm example available on elytron incubator repository: https://github.com/wildfly-security-incubator/elytron-examples/tree/master/jaas-realm