Sometimes I get surprised with the lack of information and/or documentation around wildfly and JBoss. If you search over the security documentation for wildfly, it supports several authentication mechanisms, by default BASIC, FORM, SPNEGO and CLIENT-CERT. There is no mention to bearer tokens or JWT but, surprise, surprise, the support is there and it can be used out of the box. There is very scarce documentation but you can find some examples and a quickstart about this feature.
A JSON Web Token (JWT) is an access token than contains three parts: a header, a payload and a signature. The header is just a JSON with some fixed fields (like type -jwt in this case-, algorithm of the signature or the id of the key used). The payload is the interesting part and is just any JSON with the information that the token is going to interchange, it can be any information, but things like username, roles, groups, name and surname are common. Among those fields there are two typical keys: iss or issuer and aud or audience. The issuer is who is issuing the token. The audience is for what endpoint the token was created. Wildfly implementation can be configured to check these two fields. Finally a signature of the header and payload is added to ensure that the token is valid and has not been modified (non-repudiation and integrity). In current days these tokens are ubiquitous to authenticate and authorize web services applications.
I had no idea about this feature (even more, I thought that there was no bearer authentication by default in wildfly) and I had recommended custom developments several times for this use-case. That was a wrong advise or, at least, an incomplete one. So, once I have heard about this mechanism I thouhgt that it was fair to test it and know more about how it works. This entry is going to test the new BEARER_TOKEN mechanism introduced by elytron in a wildfly 13. The JWT token is going to be provided by the keycloak project but the server is going to consume it just using elytron (no agent provided by keycloak is going to be used, just plain wildfly BEARER_TOKEN mechanism).
The steps to setup the demo are the following.
First download and install wildfly 13.
unzip wildfly-13.0.0.Final.zip
cd wildfly-13.0.0.Final/bin/
./add-user.sh -u admin -p XXXXX
./standalone.sh
Configure with offset 10000 (keycloak will be running at default ports):
./jboss-cli.sh --connect
/socket-binding-group=standard-sockets:write-attribute(name=port-offset, value="${jboss.socket.binding.port-offset:10000}")
reload
Now just do the same for keycloak.
unzip keycloak-4.1.0.Final.zip
cd keycloak-4.1.0.Final/bin/
./add-user.sh -u admin -p XXXXX
./add-user-keycloak.sh -r master -u admin -p XXXXX
./standalone.sh
Using the keycloak administration several objects are created. First a role that will be assigned to our users. In Roles click Add Role and create a user role.
Then create the user to be used with that role. Go to Users and Add user.
Assign a password in the Credentials tab (non-temporary) and finally assign the user role to it.
In Clients click Create and fill the wildlfy client information. This client will be used to get the token for the application (using direct login or direct grants) and with that token the wildfly endpoint will be called.
When you configure a client as confidential in the second tab, Credentials, you can set a secret for the client. This password will be needed for some operations later on (for login using client credentials and to configure introspection).
In the Scope tab full scope is removed and only the role user is assigned. This way the JWT token is restricted to contain only this role (if the user has it) and not all the user information.
Finally a mapper is going to be configured. This part is the only tricky point of the demo. The wildfly authenticator expects the roles in a top-level field in the JWT token, but keycloak uses the claim realm_access.roles (two levels). For that reason the role list is going to be moved to a Roles field (in elytron this is the default attribute is for roles, although any another attribute can be configured using a role-decoder). Create protocol mapper in the Mappers tab.
Now that keycloak is configured, we need the certificate that is going to be used to sign the token. We can get it from keycloak in Realm Settings → Keys → RSA type → Click Certificate. Create a keystore with it. The base64 data should be enclosed with -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- lines to be in a PEM format (the file keycloak.pem was generated).
cd ${WILDFLY_HOME}/standalone/configuration
keytool -importcert -keystore keycloak.jks -storepass changeit -file keycloak.pem -alias keycloak -trustcacerts
Going back to wildfly, elytron should be configured now to create a new domain using a token-realm. In a first step the realm is going to be configured to check a jwt token.
/subsystem=elytron/key-store=jwt-key-store:add(type="JKS", relative-to=jboss.server.config.dir, path="keycloak.jks", credential-reference={clear-text="changeit"})
/subsystem=elytron/token-realm=jwt-realm:add(jwt={issuer=["http://localhost:8080/auth/realms/master"], audience=["wildfly"], key-store=jwt-key-store, certificate="keycloak"}, principal-claim="preferred_username")
/subsystem=elytron/security-domain=jwt-domain:add(realms=[{realm=jwt-realm}], permission-mapper=default-permission-mapper, default-realm=jwt-realm)
/subsystem=elytron/http-authentication-factory=jwt-http-authentication:add(security-domain=jwt-domain, http-server-mechanism-factory=global, mechanism-configurations=[{mechanism-name="BEARER_TOKEN", mechanism-realm-configurations=[{realm-name="jwt-realm"}]}])
/subsystem=undertow/application-security-domain=jwt-domain:add(http-authentication-factory=jwt-http-authentication)
As commented before the jwt is configured to only admit access tokens that are emitted by a set of issuers and to a specific audience (it seems those two fields are optional and can be omitted). The key-store is used to reference the certificate that keycloak is going to use to sign the tokens. This way elytron will validate that the signature is valid, being sure that the token was generated by keycloak and not modified by someone else. The attribute to identify the user will be preferred_username and nothing is specified for roles, so the default Roles attribute will be used.
Finally some debug levels are configured to check that everything is working.
/subsystem=logging/logger=org.wildfly.security:add(level=ALL)
/subsystem=logging/logger=org.wildfly.elytron:add(level=ALL)
A hello-world application is developed to use jaxrs and return the name of the logged user. The endpoint is as simple as this one.
package es.rickyepoderi.sample;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@Path("hello")
public class HelloWorld {
@Context
private HttpServletRequest request;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String world() {
return "Hello " + (request.getRemoteUser() != null? request.getRemoteUser() : "world") + "!!!";
}
}
In order to use the previous configuration the web.xml should use a security-constraint and the authentication method should be set to BEARER_TOKEN. Besides the user role is configured to give access to the application (this way the application is ensuring that token roles are parsed correctly).
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<security-constraint>
<web-resource-collection>
<web-resource-name>Protect all application</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>BEARER_TOKEN>/auth-method>
<realm-name>jwt-domain</realm-name>
</login-config>
<security-role>
<description>Role required to log in to the Application>/description>
<role-name>user</role-name>
</security-role>
<session-config>
<session-timeout>30</session-timeout>
</session-config>
</web-app>
In the jboss-web.xml just the domain added to undertow is configured.
<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
<security-domain>jwt-domain</security-domain>
</jboss-web>
Now a little test.sh is created in order to test the endpoint (you can find it inside the maven project). A curl command is executed to login and get a token using direct grants over the keycloak client. That token is sent inside the Authorization header to call the wildfly endpoint. The script is very simple but it works.
bash test.sh
Hello ricky!!!
The hello endpoint is returning my username so the token is being correctly validated and the username and roles are obtained.
Now let's try the introspection. The introspect is an endpoint that an OAuth 2.0 server gives to validate access tokens. This way, instead of using wildfly internal code, the container will call this endpoint to validate the access token. This has some advantages, for example, that the token is not restricted to be a JWT one.
So now the configuration of the jwt-realm should be modified from jwt to oauth2-introspect. The elytron configuration just offers to use a username/password configuration using BASIC authentication, but, as I commented previously, this is the default authentication for a confidential keycloak client, so it should work. The username is the client name, wildfly, and the password the one generated in the Credentials tab when the client was configured.
/subsystem=elytron/token-realm=jwt-realm:undefine-attribute(name=jwt)
/subsystem=elytron/token-realm=jwt-realm:write-attribute(name=oauth2-introspection, value={introspection-url=http://localhost:8080/auth/realms/master/protocol/openid-connect/token/introspect, client-id=wildfly, client-secret=39917c0f-139a-42d2-af03-2b4207758242})
And my little script continues returning the same output, therefore the introspection endpoint is validating the ticket as expected.
So, it is working. Wildfly and the new elytron subsystem has a poor documented mechanism called BEARER_TOKEN, which lets you authenticate using JWT tokens (and, developing a custom realm, can be extended to more tokens, in general, to any token sent in the Authorization header with the BEARER prefix). In this entry the keycloak single sign-on product was installed to obtain the JWT token and then a token realm was configured inside elytron to validate it. First an internal validation was done (the wildfly code checks issuer, audience and signature) and then an OAuth introspection endpoint was configured to do the same (the server just calls the endpoint to validate the token). Both configuration work. Remember that the only trick was moving the roles to first level claim (Roles in this case), that is needed because wildfly just considers attributes at first level (and keycloak adds roles by default in a more complex JSON document). After my warning some documentation is starting to come to the project.
Tokenized regards!
Comments