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.
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+
-
Docker
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.
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 {quickstarts-clone-url}
, or download an {quickstarts-archive-url}[archive].
The solution is located in the using-keycloak
{quickstarts-tree-url}/using-keycloak[directory].
First, we need a new project. Create a new project with the following command:
mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}: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.
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.
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.
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
{
"realm": "quarkus",
"auth-server-url": "http://localhost:8180/auth",
"resource": "backend-service",
"bearer-only" : true,
"credentials": {
"secret": "secret"
},
"policy-enforcer": {
"enforcement-mode": "PERMISSIVE"
}
}
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.
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 {quickstarts-tree-url}/using-keycloak/config/quarkus-realm.json[realm configuration file] to create a new realm. For more details, see the Keycloak documentation about how to create a new realm.
To run the microservice in dev mode, use ./mvnw clean compile quarkus:dev
.
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
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
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' \
)
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.