Quarkus - Using Keycloak to Protect JAX-RS Applications

This guide demonstrates how your Quarkus application can use Keycloak to protect your JAX-RS applications using bearer token authorization, where these tokens are issued by a Keycloak Server.

Bearer Token Authorization is the process of authorizing HTTP requests based on the existence and validity of a bearer token representing a subject and his access context, where the token provides valuable information to determine the subject of the call as well whether or not a HTTP resource can be accessed.

Keycloak is a OAuth 2.0 compliant Authorization Server, capable of issuing access tokens so that you can use them to access protected resources. We are not going to enter into the details on what OAuth 2.0 is and how it works but give you a guideline on how to use OAuth 2.0 in your JAX-RS applications using the Quarkus Keycloak Extension.

If you are already familiar with Keycloak, you’ll notice that the extension is basically another adapter implementation but specific for Quarkus applications. Otherwise, you can find more information in Keycloak documentation.

Prerequisites

To complete this guide, you need:

  • less than 15 minutes

  • an IDE

  • JDK 1.8+ installed with JAVA_HOME configured appropriately

  • Apache Maven 3.5.3+

  • jq tool

  • Docker

Architecture

In this example, we build a very simple microservice which offers three endpoints:

  • /api/users/me

  • /api/admin

  • /api/confidential

These endpoints are protected and can only be accessed if a client is sending a bearer token along with the request, which must be valid (e.g.: signature, expiration and audience) and trusted by the microservice.

The bearer token is issued by a Keycloak Server and represents the subject to which the token was issued for. For being an OAuth 2.0 Authorization Server, the token also references the client acting on behalf of the user.

The /api/users/me endpoint can be accessed by any user with a valid token. As a response, it returns a JSON document with details about the user where these details are obtained from the information carried on the token.

The /api/admin endpoint is protected with RBAC (Role-Based Access Control) where only users granted with the admin role can access. At this endpoint, we use the @RolesAllowed annotation to declaratively enforce the access constraint.

The /api/confidential endpoint is also protected with RBAC. However, instead of using a declarative approach, we are externalizing authorization where the decision to whether or not access should be granted is delegated to policies managed in the Keycloak Server.

Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

Clone the Git repository: git clone https://github.com/quarkusio/quarkus-quickstarts.git, or download an archive.

The solution is located in the using-keycloak directory.

Creating the Maven Project

First, we need a new project. Create a new project with the following command:

mvn io.quarkus:quarkus-maven-plugin:0.22.0:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=using-keycloak \
    -Dextensions="keycloak, resteasy-jsonb"

This command generates a Maven project, importing the keycloak extension which is an implementation of a Keycloak Adapter for Quarkus applications and provides all the necessary capabilities to integrate with a Keycloak Server and perform bearer token authorization.

Writing the application

Let’s start by implementing the /api/users/me endpoint. As you can see from the source code below it is just a regular JAX-RS resource:

package org.acme.keycloak;

import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.KeycloakSecurityContext;

public class UsersResource {

    @Inject
    KeycloakSecurityContext keycloakSecurityContext;

    @GET
    @Path("/me")
    @RolesAllowed("user")
    @Produces(MediaType.APPLICATION_JSON)
    @NoCache
    public User me() {
        return new User(keycloakSecurityContext);
    }

    public class User {

        private final String userName;

        User(KeycloakSecurityContext securityContext) {
            this.userName = securityContext.getToken().getPreferredUsername();
        }

        public String getUserName() {
            return userName;
        }
    }
}

Note that the source code above defines an injection point as follows:

@Inject
KeycloakSecurityContext keycloakSecurityContext;

The KeycloakSecurityContext is an object produced by the Keycloak extension that you can use to obtain information from tokens sent to your application. In the source code above we are using this object to access the token representation and obtain the username of the user represented by the token.

The source code for the /api/admin endpoint is also very simple. The main difference here is that we are using a @RolesAllowed annotation to make sure that only users granted with the admin role can access the endpoint:

package org.acme.keycloak;

import javax.annotation.security.RolesAllowed;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/api/admin")
public class AdminResource {

    @GET
    @RolesAllowed("admin")
    @Produces(MediaType.TEXT_PLAIN)
    public String admin() {
        return "granted";
    }
}

For last, the /api/confidential endpoint. As you can see from the source code below, there is no explicit access control defined to this endpoint. The Keycloak extension will enforce access to this endpoint based on the policies defined in the Keycloak Server:

package org.acme.keycloak;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/api/confidential")
public class ConfidentialResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String confidential() {
        return "confidential";
    }
}

For now, don’t worry about how the extension enforces access to the /api/confidential. Just keep in mind that there are some configuration that we need to define to make this happen.

Configuring the application

The Keycloak extension allows you to define the adapter configuration using either the application.properties file or using a keycloak.json. Both files should be located at the src/main/resources directory.

Configuring using the application.properties file

quarkus.keycloak.realm=quarkus
quarkus.keycloak.auth-server-url=http://localhost:8180/auth
quarkus.keycloak.resource=backend-service
quarkus.keycloak.bearer-only=true
quarkus.keycloak.credentials.secret=secret
quarkus.keycloak.policy-enforcer.enable=true
quarkus.keycloak.policy-enforcer.enforcement-mode=PERMISSIVE

Configuring using the keycloak.json file

{
  "realm": "quarkus",
  "auth-server-url": "http://localhost:8180/auth",
  "resource": "backend-service",
  "bearer-only" : true,
  "credentials": {
    "secret": "secret"
  },
  "policy-enforcer": {
    "enforcement-mode": "PERMISSIVE"
  }
}

Configuring CORS

If you plan to consume this application from a Web Application running on a different domain, you will need to configure CORS (Cross-origin resource sharing). The Keycloak extension have an option for that :

quarkus.keycloak.enable-cors=true

Or if you are using the keycloak.json format :

{
  ...
  "enable-cors" : true
}

For most use case that will be enough but if you need a more fined-grained CORS configuration you can set these different options :

quarkus.keycloak.cors-max-age=1000
quarkus.keycloak.cors-allowed-methods=POST, PUT, DELETE, GET
quarkus.keycloak.cors-exposed-headers=WWW-Authenticate, My-custom-exposed-Header
{
  "cors-max-age" : 1000,
  "cors-allowed-methods" : "POST, PUT, DELETE, GET",
  "cors-exposed-headers" : "WWW-Authenticate, My-custom-exposed-Header"
}

For more details about this file and all the supported options, please take a look at Keycloak Adapter Config.

Starting and Configuring the Keycloak Server

To start a Keycloak Server you can use Docker and just run the following command:

docker run --name keycloak -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak

You should be able to access your Keycloak Server at localhost:8180/auth.

Log in as the admin user to access the Keycloak Administration Console. Username should be admin and password admin.

Import the realm configuration file to create a new realm. For more details, see the Keycloak documentation about how to create a new realm.

Running and Using the Application

Running in Developer Mode

To run the microservice in dev mode, use ./mvnw clean compile quarkus:dev.

Running in JVM Mode

When you’re done playing with "dev-mode" you can run it as a standard Java application.

First compile it:

./mvnw package

Then run it:

java -jar ./target/using-keycloak-runner.jar

Runing in Native Mode

This same demo can be compiled into native code: no modifications required.

This implies that you no longer need to install a JVM on your production environment, as the runtime technology is included in the produced binary, and optimized to run with minimal resource overhead.

Compilation will take a bit longer, so this step is disabled by default; let’s build again by enabling the native profile:

./mvnw package -Pnative

After getting a cup of coffee, you’ll be able to run this binary directly:

./target/using-keycloak-runner

Testing the Application

The application is using bearer token authorization and the first thing to do is obtain an access token from the Keycloak Server in order to access the application resources:

export access_token=$(\
    curl -X POST http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/token \
    --user backend-service:secret \
    -H 'content-type: application/x-www-form-urlencoded' \
    -d 'username=alice&password=alice&grant_type=password' | jq --raw-output '.access_token' \
 )

The example above obtains an access token for user alice.

Any user is allowed to access the http://localhost:8080/api/users/me endpoint which basically returns a JSON payload with details about the user.

curl -v -X GET \
  http://localhost:8080/api/users/me \
  -H "Authorization: Bearer "$access_token

The http://localhost:8080/api/admin endpoint can only be accessed by users with the admin role. If you try to access this endpoint with the previously issued access token, you should get a 403 response from the server.

 curl -v -X GET \
   http://localhost:8080/api/admin \
   -H "Authorization: Bearer "$access_token

In order to access the admin endpoint you should obtain a token for the admin user:

export access_token=$(\
    curl -X POST http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/token \
    --user backend-service:secret \
    -H 'content-type: application/x-www-form-urlencoded' \
    -d 'username=admin&password=admin&grant_type=password' | jq --raw-output '.access_token' \
 )

The http://localhost:8080/api/confidential endpoint is protected with a policy defined in the Keycloak Server. The policy only grants access to the resource if the user is granted with a confidential role. The difference here is that the application is delegating the access decision to Keycloak. To access the confidential endpoint, you should obtain an access token for user jdoe:

export access_token=$(\
 curl -X POST http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/token \
 --user backend-service:secret \
 -H 'content-type: application/x-www-form-urlencoded' \
 -d 'username=jdoe&password=jdoe&grant_type=password' | jq --raw-output '.access_token' \
)

Multi-Tenant Deployments

The Keycloak Quarkus Extension also supports multitenancy. This capability is specially useful if your service is serving different tenants each one referencing a specific Keycloak Realm and configuration.

Multitenancy is all about dynamically choosing the proper configuration depending on the characteristics of incoming requests. This is accomplished by implementing the KeycloakConfigResolver interface as follows:

package org.acme.keycloak;

import javax.enterprise.context.ApplicationScoped;

import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.spi.HttpFacade;

@ApplicationScoped
public class TenantConfigResolver implements KeycloakConfigResolver {
    @Override
    public KeycloakDeployment resolve(HttpFacade.Request facade) {
        String tenant = facade.getHeader("tenant");

        switch (tenant) {
            case "tenant-1":
                // create a deployment config for tenant-1
            case "tenant-2":
                // create a deployment config for tenant-1
            case "tenant-3":
                // create a deployment config for tenant-1
        }

        // if not resolved, returning null means using the default configuration from keycloak.json
        return null;
    }
}

A KeycloakConfigResolver is responsible to produce a KeycloakDeployment defining the configuration that should be used to process incoming requests, so that you can dynamically change the realm or any other configuration on a per-request basis.