How to create a custom Elytron Realm

In this tutorial we will learn how to create a custom Realm in Elytron, which is the equivalent of the old legacy Login Modules, and we will test it with a sample Web application.

The starting point for creating a custom Ream in Elytron is the interface SecurityRealm which contains the contract for a realm backed by a single homogeneous store of identities and credentials.

We will not cover all interface methods available in this interface, but the most important one which is:

public RealmIdentity getRealmIdentity(final Principal principal) throws RealmUnavailableException {
}

Within this method, you can either return a RealmIdentity if the login/password match or a RealmIdentity.NON_EXISTENT.

Most Realm implementations are an extension of the CacheableSecurityRealm which supports caching of org.wildfly.security.auth.server.RealmIdentity instances.

In the next section we will show how to implement a CacheableSecurityRealm to define a custom Realm which contains a set of hardcoded credentials that can be combined with entries from the configuration.

Creating a custom Realm

To get started, we will create a custom Realm named com.example.customrealm.ExampleRealm which implements CacheableSecurityRealm and Configurable to allow realm configuration. Here is our implementation:

public class ExampleRealm implements CacheableSecurityRealm, Configurable {
    
    private Map<String, String> users;
    private Map<String, Set<String>> roles;

    
    public ExampleRealm() {
        // nothing
    }
    
    public ExampleRealm(Map<String,String> map) {
        // test
        initialize(map);
    }
    
    @Override
    public void initialize(Map<String, String> map) {
        users = new HashMap<>();
        roles = new HashMap<>();

        //Adding Guest users
        String guest[] = new String[] { "Guest"};
        for (Map.Entry<String, String> entry : map.entrySet()) {
            // user and password
            users.put(entry.getKey(),entry.getValue());
            roles.put(entry.getKey(),new HashSet(Arrays.asList(guest)));
        }

        // Adding other users
        String array[] = new String[] { "Admin", "Guest"};
        users.put("admin","admin");
        roles.put("admin",new HashSet(Arrays.asList(array)));

        System.out.println("I've loaded "+users.size()+ " users ");
    }
    
    @Override
    public void registerIdentityChangeListener(Consumer<Principal> cnsmr) {
        // nothing
    }
    

    @Override
    public SupportLevel getCredentialAcquireSupport(Class<? extends Credential> credentialType, String algorithmName,
            AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException {
        return SupportLevel.UNSUPPORTED;
    }


    @Override
    public SupportLevel getEvidenceVerifySupport(Class<? extends Evidence> evidenceType, String algorithmName)
            throws RealmUnavailableException {
        return PasswordGuessEvidence.class.isAssignableFrom(evidenceType)? SupportLevel.POSSIBLY_SUPPORTED : SupportLevel.UNSUPPORTED;
    }

    @Override
    public RealmIdentity getRealmIdentity(final Principal principal) throws RealmUnavailableException {
        // just search the user in the configured users
        String password = users.get(principal.getName());
        if (password != null) {
            return new ExampleRealmIdentity(principal, password, roles.get(principal.getName()));
        }
        return RealmIdentity.NON_EXISTENT;
    }

    @Override
    public String toString() {
        return "ExampleRealm: " + this.users.keySet().stream().collect(Collectors.toList());
    }
    
}

So, the purpose of this Custom Realm is to create some cached user/password entries which are bound to a set of Roles. We have added only one user with credentials “admin”/”admin” bound to the Roles “Admin” and “Guest”. In addition, the initialize method collects a set of key/values attributes from the configuration which are allowed only the “Guest” Role.

Realm Identities need to implement the org.wildfly.security.auth.server.RealmIdentity to store credentials and roles. The basic implementation of our RealmIdentity follows here:

public class ExampleRealmIdentity implements RealmIdentity {

    private final Principal principal;
    private final String password;
    private final Set<String> roles;
    
    public ExampleRealmIdentity(Principal principal, String password, Set<String> roles) {
        this.principal = principal;
        this.password = password;
        this.roles = roles;
    }
    
    @Override
    public Principal getRealmIdentityPrincipal() {
        return principal;
    }

    @Override
    public SupportLevel getCredentialAcquireSupport(Class<? extends Credential> credentialType,
            String algorithmName, AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException {
        // do not support credential acquire
        return SupportLevel.UNSUPPORTED;
    }

    @Override
    public <C extends Credential> C getCredential(Class<C> credentialType) throws RealmUnavailableException {
        // do not return credentials
        return null;
    }

    @Override
    public SupportLevel getEvidenceVerifySupport(Class<? extends Evidence> evidenceType, String algorithmName)
            throws RealmUnavailableException {
        return PasswordGuessEvidence.class.isAssignableFrom(evidenceType)? SupportLevel.SUPPORTED : SupportLevel.UNSUPPORTED;
    }

    @Override
    public boolean verifyEvidence(Evidence evidence) throws RealmUnavailableException {
        if (evidence instanceof PasswordGuessEvidence) {
            PasswordGuessEvidence guess = (PasswordGuessEvidence) evidence;
            return Arrays.equals(password.toCharArray(), guess.getGuess());
        }
        return false;
    }

    @Override
    public boolean exists() throws RealmUnavailableException {
        return true;
    }
    
    @Override
    public Attributes getAttributes() throws RealmUnavailableException {
        if (roles == null || roles.isEmpty()) {
            return Attributes.EMPTY;
        }
        MapAttributes map = new MapAttributes();
        map.addAll("Roles", roles);
        return map;
    }
    
    @Override
    public AuthorizationIdentity getAuthorizationIdentity() throws RealmUnavailableException {
        return AuthorizationIdentity.basicIdentity(getAttributes());
    }
}

Building and deploying the Custom Realm

The Realm is part of a Maven project which is avaiable here: https://github.com/fmarchioni/mastertheboss/tree/master/security/example-elytron-realm

You can build it as any maven project:

$ mvn install

Then, copy the generated jar file in a folder, for example in WildFly’s bin folder:

$ cp target/elytron-custom-realm-1.0.0.jar $JBOSS_HOME/bin

We will now install the Custom Realm on Elytron and bind it to an HTTP Authentication Factory so that Web application can reference it as an Application security domain:

#Add module for the custom realm
module add --name=com.example.customrealm  --resources=elytron-custom-realm-1.0.0.jar --dependencies=org.wildfly.security.elytron,org.wildfly.extension.elytron

#Creates a custom elytron Realm and initialize one guest identity with guest/password
/subsystem=elytron/custom-realm=example-realm:add(module=com.example.customrealm, class-name=com.example.customrealm.ExampleRealm, configuration={"guest" => "password"})

#Defines a Security Domain named example-domain based om the example-realm
/subsystem=elytron/security-domain=example-domain:add(realms=[{realm=example-realm}], default-realm=example-realm, permission-mapper=default-permission-mapper)

#creates an HTTP Authentication factory based on the example-domain which uses HTTP Basic authentication
/subsystem=elytron/http-authentication-factory=example-http-auth:add(http-server-mechanism-factory=global, security-domain=example-domain, mechanism-configurations=[{mechanism-name=BASIC, mechanism-realm-configurations=[{realm-name=example-domain}]}])

#Adds the example-domain to undertow's security domain
/subsystem=undertow/application-security-domain=example-domain:add(http-authentication-factory=example-http-auth)

In addition, if you are running applications based on the ejb subsystem, the security domain needs to be added to the ejb3 subsystem as well:

/subsystem=ejb3/application-security-domain=example-domain:add(security-domain=example-domain)

Testing the Elytron custom Realm

To test our Realm, we will deploy a simple Web application which is configured to use the above Security Realm:

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

This example Web application is available at: https://github.com/fmarchioni/mastertheboss/tree/master/security/ee-security-elytron

Deploy it as follows:

$ mvn install wildfly:deploy

Then, login at http://localhost:8080/ee-security-elytron using the credentials “admin”/”admin”. You will be granted access:

Congratulations! You just managed to create,deploy and test a Custom Elytron Realm on WildFly!