Keycloak as OpenID Connect Provider

A working example using Keycloak, Angular and Java (MicroProfile)

Disclaimer: The git repository for this guide can be found here.

Photo by Bich Tran

Introduction

Authentication and Authorization are such ubiquitous concepts when designing and implementing software, that it is baffling how often those concepts — and their implementation — are misunderstood.

Authentication is the process of ensuring that someone is indeed whom he claims to be. There are multiple ways of performing authentication when it comes to software, such as password validation, certificates, magic links, et cetera and so forth.

Authorization, on the other hand, is the process of making sure that someone only is able to access what he is allowed to access. For example, a certain user can only update his own user details, but not the details of other users.

Although most systems need some sort of Authentication and Authorization scheme, implementing it is far from trivial. Account and role management, third-party identity providers, terms and conditions acceptance, email address confirmation, among many other requirements may make this all very time-consuming.

For this, in 2014, Keycloak was released as an “open source Identity and Access Management solution aimed at modern applications and services”, implementing standard specifications such as OpenID Connect, OAuth 2.0 and SAML. It also supports out of the box identity brokering through popular social login services and third-party identity providers that follow standard specifications. Besides this, it includes configurations to manage user roles and permissions, besides many other features.

In this article, it is provided a full example of how to use Keycloak for authentication and authorization on the communication between a front-end Angular application and a Java back-end service. While the linked repository contains instructions on how to run the example, this article intends to provide an in-depth explanation of how it works.

Project Structure

The guide consists of a Maven project with three modules:

  • Authorization Server
  • Resource Server
  • Client Application

Each module contains a Dockerfile, and the project as a whole can be deployed with Docker Compose. The docker-compose.yml file creates a Docker cluster and binds each container to a different host port, in order to simulate a real-life scenario as if each module was a different application running in a different server.

Authorization Server

The Authorization Server is represented by the keycloak-server module. It extends the official Docker image, as shown in the code snippet below:

FROM jboss/keycloak:4.8.3.Final
# Set environment variables.
ENV DB_VENDOR h2
# Copy realm files to import at startup.
COPY realm/example-realm-export.json .
# Set startup options.
CMD ["-b", "0.0.0.0", "-c", "standalone.xml", \
"-Dkeycloak.import=example-realm-export.json"]

The above snippet of this module’s Dockerfile starts out by setting H2 as the underlying database for Keycloak. It is not meant for production environments, but it is suitable for this example — any changes on the configuration can be reverted by simply removing the container and running it again.

Afterward, it copies the example-realm-export.json file to the container. This is basically a database export file that was generated and that will be used to create the Example realm within Keycloak, containing some configuration suitable for this project. How to generate database export files is a topic for a different guide, and will not be covered here.

Lastly, it starts up Keycloak in standalone mode and sets a System Property keycloak.import indicating that the realm file must be imported into the database at startup time.

Realms are the way Keycloak manages sets of users, credentials, roles, groups, and configuration metadata. Realms are completely isolated from one another, in the sense of how data is stored and managed within Keycloak. Keycloak comes with a Master realm out of the box, but this realm is meant to be used solely for administration purposes. With this in mind, a new realm is created for this project: The Example realm.

The Example realm comes with a client rng_app already configured to generate a JWT that conforms to the MicroProfile Interoperable JWT RBAC specification. It also comes with a few users configured for authentication.

For example when authenticating one of the users with the rng_appclient on the Example realm, the following JWT is generated as Access Token:

eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJpMFNTVDRQV1o2WDNuZm9rWFVzMjMyeXdFQU1Jb3IzZExYUWdwQVdJdnhnIn0.eyJqdGkiOiJiZmRiMWZkNy1mODg5LTQ3MGEtYmFiNC1lYjNmZGE3YzVmODgiLCJleHAiOjE1NTc1MjIxMzAsIm5iZiI6MCwiaWF0IjoxNTU3NTIxODMwLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgxODAvYXV0aC9yZWFsbXMvZXhhbXBsZSIsInN1YiI6IjdmOGY3M2E5LTI5YmItNDAxMS1hM2FlLTA2ZDk0M2YzZTk0NiIsInR5cCI6IkJlYXJlciIsImF6cCI6InJuZ19hcHAiLCJhdXRoX3RpbWUiOjAsInNlc3Npb25fc3RhdGUiOiI5ZGQzNDU3Ni0wOGU2LTQ2ZGUtOGQyMi1lOTU1MTQxMDBiOGEiLCJhY3IiOiIxIiwic2NvcGUiOiJwcm9maWxlIGdyb3VwcyBlbWFpbCIsInVwbiI6ImFsbEB1c2VyLnRlc3QiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJBbGxhbiBBbGwiLCJncm91cHMiOlsiZHVtbXkiLCJybmciXSwiZ2l2ZW5fbmFtZSI6IkFsbGFuIiwiZmFtaWx5X25hbWUiOiJBbGwiLCJlbWFpbCI6ImFsbEB1c2VyLnRlc3QifQ.jDs_Wza3Ifo6PEDIh2jgreWOFydKyZT0p04NeubCzOUEQM8dvMCpxLdr2oNrcw8CZuWO7Uv2CHNMXCFey1zjb1vSOV9YKspZ3lxbdzmIl078PDk5quciQLs2PqD6vfw5B1QhwiJhhclgKwAfe3DIFY5MyRY1k1kpw3q31gtD_o-QZMpqmfzHruQ9m6cAmzPJb44UqA9YIY3F5lYBq3N1_ZEYGTUWIuLrpizJucwg6LLvrwmTMPeHBVqzOFvwarbLd_smrjVIejDMkYwFcgMXFn5_cTun4HsNWPA1rCTkjpkykZCjIUST2Ryc_sgc64BLMnr06bp3As_5fOygBtkZYA

This JWT contains the following Payload:

{
"jti": "c50939a5-6186-4b97-b13b-c4fa11cc8cf3",
"exp": 1557522447,
"nbf": 0,
"iat": 1557522147,
"iss": "http://localhost:8180/auth/realms/example",
"sub": "af3c8ca3-799c-4714-980d-d74c31f06020",
"typ": "Bearer",
"azp": "rng_app",
"auth_time": 0,
"session_state": "d6b70050-70ea-4a13-a87b-3a6b74caeda4",
"acr": "1",
"scope": "profile groups email",
"upn": "dummy@user.test",
"email_verified": false,
"name": "Danny Dummy",
"groups": [
"dummy"
],
"given_name": "Danny",
"family_name": "Dummy",
"email": "dummy@user.test"
}

The JWT is used for Authentication and Authorization, following OpenID Connect standards, on the Resource Server.

Resource Server

The Resource Server is a small JEE application deployed on Payara-Micro that exposes one single endpoint: GET /resource-server/api/rng . This endpoint requires authentication and the user must have the correct roles. For this the official Docker image for Payara-Micro is extended, as shown in the code snippet below:

FROM payara/micro:5.191
# Root user to install bash to the container.
USER root
# Update and install bash.
RUN apk update && apk add bash
# Go back to default user.
USER payara
# Add files needed for deployment to container.
COPY target/resource-server.war /opt/payara/deployments/
ADD --chown=payara:payara docker-entrypoint.sh ./
ADD --chown=payara:payara start-payara.sh ./
# Make sure files have correct permissions.
RUN ["chmod", "755", "./docker-entrypoint.sh"]
RUN ["chmod", "755", "./start-payara.sh"]
# Start application.
ENTRYPOINT ["./docker-entrypoint.sh"]
CMD ["./start-payara.sh"]

Normally, it would suffice to merely copy the WAR package to the opt/payara/deployments/ folder. However, some System Properties must be set at server startup, and for that a couple of shell scripts are added: docker-entrypoint.sh and start-payara.sh . Those System Properties can be seen on start-payara.sh :

#!/usr/bin/env bash

java \
-Dmp.jwt.verify.publickey.location=${MP_JWT_VERIFY_PUBLICKEY_LOCATION} \
-Dmp.jwt.verify.issuer=${MP_JWT_VERIFY_ISSUER} \
-jar /opt/payara/payara-micro.jar \
--deploymentDir /opt/payara/deployments \
--noCluster \
--port 8080

The System Properties mp.jwt.verify.publickey.location and mp.jwt.verify.issuer are used by the MicroProfile dependency to perform authentication. The issuer set as an environment variable must match the value sent on the iss claim of the token — in this case, http://localhost:8180/auth/realms/example. The public key location set as an environment variable must be the URL of the endpoint of the Authorization Server containing the JWKS (public keys) used to validate the JWT signature — in this case, http://localhost:8180/auth/realms/example/protocol/openid-connect/certs .

For the MicroProfile authentication, a few things must be set in the Java code. First, the Application class must contain some annotations:

@ApplicationPath("api")
@ApplicationScoped
@LoginConfig(authMethod = "MP-JWT")
@DeclareRoles({"rng"})
public class ApplicationConfig extends Application {
  • @LoginConfig(authMethod = "MP-JWT") : This configures the authentication mechanism of the application to authenticate bearer tokens in JWT format following the MicroProfile specification.
  • @DeclareRoles({“rng”}) : This declares all roles recognized by this application, and is necessary for authorization.

The controller class for the REST endpoint also must have some annotations:

@Path("rng")
@DenyAll
@RequestScoped
@Slf4j
public class RNGController {
  • @DenyAll : This annotation indicates that no security roles are allowed to invoke methods of this class.

This is overridden by an annotation on the method itself:

@GET
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed({"rng"})
public Response generate(
  • @RolesAllowed({"rng"}) : This annotation indicates that the rng role is allowed to access the endpoint represented by this method. This role should be present in the groups claim on the JWT’s payload.

Client Application

The Client Application is a bare-bones Angular application that consists of a single login screen:

Client Application initial page

When the user clicks on Login, he is taken the Authorization Server (Keycloak), where he can input his credentials:

Authorization Server login form

After successfully inputting his credentials, the user is taken back to the login screen, and the user’s first name is obtained from the JWT:

Client Application after login

When the user clicks on the rng button (available only after login), a request is made to the Resource Server sending the JWT in the Authorization header. As a result, a random number is obtained:

Successful request to the Resource Server

After a certain amount of time passes, the JWT will expire — the Access Token lifespan is set by default to 5 minutes. If the rng button is pressed after this, the Resource Server will give back a HTTP 401 response. The Access Token lifespan is controlled by two entries on the example-realm-export.json on the keycloak-servermodule:

"accessTokenLifespan" : 60,
"accessTokenLifespanForImplicitFlow" : 60,

The Client Application relies on the angular-oauth2-oidc library to implement the Implicit flow it uses to authenticate on Keycloak. It’s essentially an implementation of the Implicit flow example contained in the library documentation.

Conclusion

This article shows how Keycloak makes it fairly simple to implement Authentication and Authorization in the context of communication in between a Front-End Client Application and a Back-End Resource Server. The fact that it relies on OpenID Connect for Authentication and Authorization makes it possible to use available libraries that do all the heavy lifting in implementing the specification.