#HOWTO: MicroProfile JWT Authentication with Keycloak and React

F or securing your enterprise applications you have several choices which require different configuration setups. Lately, the stateless approach is the de-facto standard for securing your microservice based landscape. With the choice, your applications don’t store session data as the client sends a JWT token with each request and thus the applications know about his metadata like groups and email. To standardize the authorization workflow, OAuth 2.0 was created. On top of this specification, OpenID Connect offers a layer for clients to identify application users based on the authentication performed by an authorization server.

These authorization specifications could be implemented manually with a custom solution, but using an identity-provider which is capable of the standardized workflows is more suitable in most cases. Keycloak is such an identity-provider which is open source, based on WildFly and developed by RedHat and is used quite often. In enterprise projects, you can even connect Keycloak to an existing LDAP or Active Directory for enterprise user management.

In this blog post, I’ll show you a simple setup for a JWT authentication within a Java EE 8 application with the latest MicroProfile JWT 1.1 spec running on Payara 5.183. For the identity and access management, I am using Keycloak (4.8.2.Final) and a React (16.7) based frontend to model a straightforward system architecture. At the end you will be able to authenticate with your Keycloak user, get visual information about the metadata in the JWT and access a secured JAX-RS resource to obtain a secret message.

Setting up Keycloak

The following chapter will explain how to set up the Keycloak server from scratch. You can either follow this step-by-step guide or build an already preconfigured Keycloak server from the Dockerfile I’m providing in my GitHub repository (if you use the preconfigured version, you just have to get the public RSA key from Keycloak admin console and create a sample user).

To start the Keycloak server locally I am using the official jboss/keycloak docker image with the version 4.8.2.Final. For accessing the Keycloak internals you have to pass an admin account and password during the container startup and map the port 8080 to a local port like:

docker run -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -p 8282:8080 jboss/keycloak:4.8.2.Final

In this example, I am using my port 8282 so that there won’t be a conflict with Payara later on. After the container successfully started, you can now visit the admin panel in your browser at http://localhost:8282 and use the user and password you passed to the docker container to authenticate.

First, we have to set up a new realm, which is a logical container for managing users, credentials, roles, and groups which are then isolated from other realms. At default, only the Master realm exists and to add a new one, you have to create a new in the admin panel at the top left (while hovering over the drop-down indicator):

On the following page add the name MicroProfile and make sure the realm is enabled. Next, go to the Keys tab within the Realm Settings and copy the public key of the rsa-generated key to an editor or clipboard. We will need this key later on for verifying the JWT signature within the Java EE backend.

To provide groups for the user, you have to first create some groups. Go to Manage -> Groups -> and click on New to add a group. Enter a group name e.g. ADMIN or USER and press Save. That’s all for adding a group.

As we won’t connect an Active Directory or LDAP, we have to set up at least one user manual. Go to Users (left-side menu) and click Add User. Enter a username and hit save. To add a password, go to the Credentials tab within the user and enter a password of your choice:

Within the user settings, you can switch to the tab Groups and select an available group and hit Join to add this user to the group.

As our frontend will, later on, manage the authentication with Keycloak, we have to make sure that Keycloak is aware of this application. Therefor Keycloak offers the concept of a client, which is an entity that can request Keycloak to authenticate a user. A new client can be added in the Clients section (left-side menu) in the admin panel within our realm. Click on Add Client and enter the client id react-webapp, pick openid-connect as the protocol and http://localhost:3000 as the root URL:

As Keycloak per default won’t add the group information of a user to the JWT, we have to configure this. Within the client settings for react-webapp, go to the tab Mappers and create a new one. Select Group Membership as Mapper Type, enter group as Name and groups as Token Claim Name. Make sure the Full group path switch is disabled as we would otherwise get a slash in front of the group name (e.g. /USER):

To configure the frontend for the Keycloak authentication we need the Keycloak OIDC JSON which can be obtained at the tab Installation within our client. The JSON should look like the following:

{
  "realm": "MicroProfile",
  "auth-server-url": "http://localhost:8282/auth",
  "ssl-required": "external",
  "resource": "react-webapp",
  "public-client": true,
  "confidential-port": 0
}

That’s all for the Keycloak configuration. All your configured groups, roles and clients can be exported within the Export menu section.

Setting up the frontend

Within this chapter I’ll show you the required steps to integrate Keycloak within React to produce the following application:

 

To bootstrap a simple React app I’ve used the create-react-app CLI. In addition, I’ve added semantic-ui-react, axios, and the keycloak-js dependency:

{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "axios": "^0.18.0",
    "keycloak-js": "^4.8.1",
    "react": "^16.7.0",
    "react-dom": "^16.7.0",
    "react-scripts": "2.1.2",
    "semantic-ui-css": "^2.4.1",
    "semantic-ui-react": "^0.84.0"
  }, 
  ...
}

I’m using just the generated App component and perform the authentication with Keycloak when the component did mount:

componentDidMount = () => {
    const keycloak = Keycloak("/keycloak.json");

    keycloak.init({ onLoad: 'login-required' }).success(authenticated => {
      this.setState({ keycloak: keycloak, authenticated: authenticated });
    }).error(err => {
      alert(err);
    });
}

The required keycloak.json can be placed within the public folder and is the Keycloak OIDC JSON you get from the Keycloak admin interface (one of the last steps within setting up Keycloak). The init function will check if the user is already authenticated and if not, will redirect to the Keycloak authentication page:

The user can now enter this username and his credentials to authenticate. After he got successfully authenticated, Keycloak will redirect the user to the React app. The authentication status is then stored within the state of the App component.

In addition, I am decoding the public part of the JWT as this is just a Base64 encoded string. Therefore I am using the following function:

decodeJWT = (token) => {
    var base64Url = token.split('.')[1];
    var base64 = base64Url.replace('-', '+').replace('_', '/');
    return JSON.stringify(JSON.parse(window.atob(base64)), null, 4);
}

For accessing the secured JAX-RS endpoint, the JWT token has to be attached to the HTTP Authorization header. With axios as HTTP client this is pretty straightforward:

fetchBackendData = () => {
   axios.get('http://localhost:8080/resources/secure', { headers: { 'Authorization': ' Bearer ' + this.state.keycloak.token } })
     .then(res => this.setState({ backendData: res.data }));
}

You can find the whole JSX/HTML of the App component on GitHub.

Setting up the backend

The backend application has to verify the integrity of the passed JWT token, map the JWT claims to a Java object and the available groups to Java EE roles if the JWT is valid. All this is done by the MicroProfile JWT spec. The specification defines a set of JWT claims which have to be present and describes how the authentication is performed. You can get further information in the official spec document.

To bootstrap the Java EE 8 and Microprofile 2.0.1 application, I’m using my own Maven archetype which produces the following pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>de.rieckpil.learning</groupId>
  <artifactId>microprofile-jwt-keycloak-auth</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>
  <dependencies>
    <dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-api</artifactId>
      <version>8.0</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.eclipse.microprofile</groupId>
      <artifactId>microprofile</artifactId>
      <version>2.0.1</version>
      <type>pom</type>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-core</artifactId>
      <version>2.23.0</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <finalName>microprofile-jwt-keycloak-auth</finalName>
  </build>
  <properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <failOnMissingWebXml>false</failOnMissingWebXml>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  </properties>
</project>

As the frontend request comes from a different origin, I’ve added a simple CORS JAX-RS filter:

@Provider
public class CorsFilter implements ContainerResponseFilter {

  @Override
  public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
      throws IOException {
    responseContext.getHeaders().add("Access-Control-Allow-Origin", "*");
    responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true");
    responseContext.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization");
    responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD");
  }
}

The JAX-RS application configuration is pretty straightforward:

@LoginConfig(authMethod = "MP-JWT")
@ApplicationPath("resources")
@DeclareRoles({"USER", "ADMIN"})
public class JAXRSConfiguration extends Application {

}

The @LoginConfig(authMethod = "MP-JWT") is required to enable the MP-JWT authentication mechanism within the application server. In addition, I’m defining the available roles with the declarative annotation, which could also be part of the web.xml file.

Within the JAX-RS resource I’m providing a simple endpoint which is accessible for users within the USER group:

@Path("secure")
@Stateless
public class VerySecureResource {

  @Inject
  @ConfigProperty(name = "message")
  private String message;

  @Inject
  private JsonWebToken callerPrincipal;

  @GET
  @RolesAllowed("USER")
  public Response message() {

    System.out.println(callerPrincipal.getIssuer());
    System.out.println(callerPrincipal.getRawToken());
    System.out.println(callerPrincipal.getTokenID());

    return Response.ok(callerPrincipal.getName() + " is allowed to read message: " + message).build();
  }
}

The JsonWebToken interface is part of the MicroProfile JWT spec and holds all the JWT claims and is compatible with the JSR 375 caller Principal.

For the JWT verification, you have to configure at least the name of the issuer and the public key of the RSA signature. There are several configurations described in the spec. I am using the microprofile-config.properties file within src/main/resources/META-INF:

message=Very Secure 42!
mp.jwt.verify.publickey.location=/META-INF/orange.pem
mp.jwt.verify.issuer=http://localhost:8282/auth/realms/MicroProfile

And store the public key in the configured location like:

-----BEGIN PUBLIC KEY-----
YOUR PUBLIC KEY HERE. CAN BE OBTAINED FROM THE KEYCLOAK ADMIN INTERFACE
-----END PUBLIC KEY-----

With this configuration, you are done and can deploy the Java EE application to Payara. If you are using my Maven archetype you can deploy the application and start Payara as a Docker container with the buildAndRun batch or bash script.

You can find the whole codebase and a step-by-step guide for starting this setup locally on GitHub.

Have fun securing your applications,

Phil

1 Comment

  1. Pingback: Java Weekly, Issue 264 | Baeldung

Leave a comment

Your email address will not be published. Required fields are marked *