Quarkus OAuth2 and security with Keycloak

Quarkus OAuth2 and security with Keycloak

Quarkus OAuth2 support is based on the WildFly Elytron Security project. In this article, you will learn how to integrate your Quarkus application with the OAuth2 authorization server like Keycloak.

Before starting with Quarkus security it is worth to find out how to build microservices in Quick guide to microservices with Quarkus on OpenShift, and how to easily deploy your application on Kubernetes in Guide to Quarkus on Kubernetes.

Source code

If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my repository sample-quarkus-applications. Then go to the employee-secure-service directory, and just follow my instructions 🙂 The good idea is to read the article Guide to Quarkus with Kotlin before you move on.

Using Quarkus OAuth2 for securing endpoints

In the first step, we need to include Quarkus modules for REST and OAuth2. Of course, our applications use some other modules, but those two are required.

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-elytron-security-oauth2</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>

Let’s discuss a typical implementation of the REST controller with Quarkus. Quarkus OAuth2 provides a set of annotations for setting permissions. We can allow to call an endpoint by any user with @PermitAll annotation. The annotation @DenyAll indicates that the given endpoint cannot be accessed by anyone. We can also define a list of roles allowed for calling a given endpoint with @RolesAllowed.

The controller contains different types of CRUD methods. I defined three roles: viewer, manager, and admin. The viewer role allows calling only GET methods. The manager role allows calling GET and POST methods. Finally, the admin role allows calling all the methods. You can see the final implementation of the controller class below.

@Path("/employees")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
class EmployeeResource(val repository: EmployeeRepository) {

    @POST
    @Transactional
    @RolesAllowed(value = ["manager", "admin"])
    fun add(employee: Employee): Response {
        repository.persist(employee)
        return Response.ok(employee).status(201).build()
    }

    @DELETE
    @Path("/{id}")
    @Transactional
    @RolesAllowed("admin")
    fun delete(@PathParam id: Long) {
        repository.deleteById(id)
    }

    @GET
    @PermitAll
    fun findAll(): List<Employee> = repository.listAll()

    @GET
    @Path("/{id}")
    @RolesAllowed(value = ["manager", "admin", "viewer"])
    fun findById(@PathParam id: Long): Employee?
            = repository.findById(id)

    @GET
    @Path("/first-name/{firstName}/last-name/{lastName}")
    @RolesAllowed(value = ["manager", "admin", "viewer"])
    fun findByFirstNameAndLastName(@PathParam firstName: String,
                          @PathParam lastName: String): List<Employee>
            = repository.findByFirstNameAndLastName(firstName, lastName)

    @GET
    @Path("/salary/{salary}")
    @RolesAllowed(value = ["manager", "admin", "viewer"])
    fun findBySalary(@PathParam salary: Int): List<Employee>
            = repository.findBySalary(salary)

    @GET
    @Path("/salary-greater-than/{salary}")
    @RolesAllowed(value = ["manager", "admin", "viewer"])
    fun findBySalaryGreaterThan(@PathParam salary: Int): List<Employee>
            = repository.findBySalaryGreaterThan(salary)

}

Running Keycloak

We are running Keycloak on a Docker container. By default, Keycloak exposes API and a web console on port 8080. However, that port number must be different than the Quarkus application port, so we are overriding it with 8888. We also need to set a username and password to the admin console.

$ docker run -d --name keycloak -p 8888:8080 -e KEYCLOAK_USER=quarkus -e KEYCLOAK_PASSWORD=quarkus123 jboss/keycloak

Create client on Keycloak

First, we need to create a client with a given name. Let’s say this name is quarkus. The client credentials are used during the authorization process. It is important to choose confidential in the “Access Type” section and enable option “Direct Access Grants”.

quarkus-oauth2-keycloak-client

Then we may switch to the “Credentials” tab, and copy the client secret.

Configure Quarkus OAuth2 connection to Keycloak

In the next steps, we will use two HTTP endpoints exposed by Keycloak. First of them, token_endpoint allows you to generate new access tokens. The second endpoint introspection_endpoint is used to retrieve the active state of a token. In other words, you can use it to validate access or refresh token.

The Quarkus OAuth2 module expects three configuration properties. These are the client’s name, the client’s secret, and the address of the introspection endpoint. The last property quarkus.oauth2.role-claim is responsible for setting the name of claim used to load the roles. The list of roles is a part of the response returned by the introspection endpoint. Let’s take a look at the final list of configuration properties for integration with my local instance of Keycloak.

quarkus.oauth2.client-id=quarkus
quarkus.oauth2.client-secret=7dd4d516-e06d-4d81-b5e7-3a15debacebf
quarkus.oauth2.introspection-url=http://localhost:8888/auth/realms/master/protocol/openid-connect/token/introspect
quarkus.oauth2.role-claim=roles

Create users and roles on Keycloak

Our application uses three roles: viewer, manager, and admin. Therefore, we will create three test users on Keycloak. Each of them has a single role assigned. The manager role is a composite role, and it contains the viewer role. The same with the admin, that contains both manager and viewer. Here’s the full list of test users.

quarkus-oauth2-keycloak-users

Of course, we also need to define roles. In the picture below, I highlighted the roles used by our application.

Before proceeding to the tests, we need to do one thing. We have to edit the client scope responsible for displaying a list of roles. To do that go to the section “Client Scopes”, and then find the roles scope. After editing it, you should switch to the “Mappers” tab. Finally, you need to find and edit the “realm roles” entry. The value of a field “Token Claim Name” should be the same as the value set in the quarkus.oauth2.role-claim property. I highlighted it in the picture below. In the next section, I’ll show you how Quarkus OAuth2 retrieves roles from the introspection endpoint.

quarkus-oauth2-keycloak-clientclaim

Analyzing Quarkus OAuth2 authorization process

In the first step, we are calling the Keycloak token endpoint to obtain a valid access token. We may choose between five supported grant types. Because I want to authorize with a user password I’m setting parameter grant_type to password. We also need to set client_id, client_secret, and of course user credentials. A test user in the request visible below is test_viewer. It has the role viewer assigned.

$ curl -X POST http://localhost:8888/auth/realms/master/protocol/openid-connect/token \
-d "grant_type=password" \ 
-d "client_id=quarkus" \
-d "client_secret=7dd4d516-e06d-4d81-b5e7-3a15debacebf" \
-d "username=test_viewer" \
-d "password=123456"

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIilWRfdX...",
    "expires_in": 1800,
    "refresh_expires_in": 1800,
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2...",
    "token_type": "bearer",
    "not-before-policy": 1600100798,
    "session_state": "cf9862b0-f97a-43a7-abbb-a267fff5e71e",
    "scope": "email profile"
}

Once, we have successfully generated an access token, we may use it for authorizing requests sent to the Quarkus application. But before that, we can verify our token with the Keycloak introspect endpoint. It is an additional step. However, it shows you what type of information is returned by the introspect endpoint, which is then used by the Quarkus OAuth2 module. You can see the request and response for the token value generated in the previous step. Pay close attention to how it returns a list of user’s roles.

$ curl -X POST http://localhost:8888/auth/realms/master/protocol/openid-connect/token/introspect \
-d "token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIilWRfdX..."
-H "Authorization: Basic cXVhcmt1czo3ZGQ0ZDUxNi1lMDZkLTRkODEtYjVlNy0zYTE1ZGViYWNlYmY="

{
    "exp": 1600200132,
    "iat": 1600198332,
    "jti": "af160b82-ad41-45d3-8c7d-28096beb2509",
    "iss": "http://localhost:8888/auth/realms/master",
    "sub": "f41828f6-d597-41cb-9081-46c2d7a4d76b",
    "typ": "Bearer",
    "azp": "quarkus",
    "session_state": "0fdbbd83-35f9-4f4f-912a-c17979c2a87b",
    "preferred_username": "test_viewer",
    "email": "test_viewer@example.com",
    "email_verified": true,
    "acr": "1",
    "scope": "email profile",
    "roles": [
        "viewer"
    ],
    "client_id": "quarkus",
    "username": "test_viewer",
    "active": true
}

The generated access token is valid. So, now the only thing we need to do is to set it on the request inside the Authorization header. Role viewer is allowed for the endpoint GET /employees/{id}, so the HTTP response status is 200 OK or 204 No Content.

$ curl -v http://localhost:8080/employees/1 -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIilWRfdX..."
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /employees/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: */*
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIilWRfdX...
>
< HTTP/1.1 204 No Content
<
* Connection #0 to host localhost left intact

Now, let’s try to call the endpoint that is disallowed for the viewer role. In the request visible below, we are trying to call endpoint DELETE /employees/{id}. In line with the expectations, the HTTP response status is 403 Forbidden.

$ curl -v -X DELETE http://localhost:8080/employees/1 -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIilWRfdX..."
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /employees/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: */*
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIilWRfdX...
>
< HTTP/1.1 403 Forbidden
< Content-Length: 0
<
* Connection #0 to host localhost left intact

Conclusion

It is relatively easy to configure and implement OAuth2 support with Quarkus. However, you may spend a lot of time on Keycloak configuration. That's why I explained step-by-step how to set up OAuth2 authorization there. Enjoy 🙂

Leave a Reply