Easy SSO for Vert.x with Keycloak
TL;DR:
In this blog post you’ll learn:
- How to implement Single Sign-on with OpenID Connect
- How to use Keycloak’s OpenID Discovery to infer OpenID provider configuration
- How to obtain user information
- How to check for authorization
- How to call a Bearer protected service with an Access Token
- How to implement a form based logout
Hello Blog
This is my first post in the Vert.x Blog and I must admit that up until now I have never used Vert.x in a real project. “Why are you here?”, you might ask… Well I currently have two main hobbies, learning new things and securing apps with Keycloak. So a few days ago, I stumbled upon the Introduction to Vert.x video series on youtube by Deven Phillips and I was immediately hooked. Vert.x was a new thing for me, so the next logical step was to figure out how to secure a Vert.x app with Keycloak.
For this example I build a small web app with Vert.x that shows how to implement Single Sign-on (SSO) with Keycloak and OpenID Connect, obtain information about the current user, check for roles, call bearer protected services and properly handling logout.
Keycloak
Keycloak is a Open Source Identity and Access Management solution which provides support for OpenID Connect based Singe-Sign on, among many other things. I briefly looked for ways to securing a Vert.x app with Keycloak and quickly found an older Vert.x Keycloak integration example in this very blog. Whilst this is a good start for beginners, the example contains a few issues, e.g.:
- It uses hardcoded OpenID provider configuration
- Features a very simplistic integration (for the sake of simplicity)
- No user information used
- No logout functionality is shown
That somehow nerdsniped me a bit and so it came that, after a long day of consulting work, I sat down to create an example for a complete Keycloak integration based on Vert.x OpenID Connect / OAuth2 Support.
So let’s get started!
Keycloak Setup
To secure a Vert.x app with Keycloak we of course need a Keycloak instance. Although Keycloak has a great getting started guide I wanted to make it a bit easier to put everything together, therefore I prepared a local Keycloak docker container as described here that you can start easily, which comes with all the required configuration in place.
The preconfigured Keycloak realm named vertx
contains a demo-client
for our Vert.x web app and a set
of users for testing.
Vert.x Web App
The simple web app consists of a single Verticle
, runs on http://localhost:8090
and provides a few routes with protected resources. You can find the complete example here.
The web app contains the following routes with handlers:
/
- The unprotected index page/protected
- The protected page, which shows a greeting message, users need to login to access pages beneath this path./protected/user
- The protected user page, which shows some information about the user./protected/admin
- The protected admin page, which shows some information about the admin, only users with roleadmin
can access this page./protected/userinfo
- The protected userinfo page, obtains user information from the bearer token protected userinfo endpoint in Keycloak./logout
- The protected logout resource, which triggers the user logout.
Running the app
To run the app, we need to build our app via:
This creates a runnable jar, which we can run via:
Note, that you need to start Keycloak, since our app will try to fetch configuration from Keycloak.
If the application is running, just browse to: http://localhost:8090/
.
An example interaction with the app can be seen in the following gif:
Router, SessionStore and CSRF Protection
We start the configuration of our web app by creating a Router
where we can add custom handler functions for our routes.
To properly handle the authentication state we need to create a SessionStore
and attach it to the Router
.
The SessionStore
is used by our OAuth2/OpenID Connect infrastructure to associate authentication information with a session.
By the way, the SessionStore
can also be clustered if you need to distribute the server-side state.
Note that if you want to keep your server stateless but still want to support clustering,
then you could provide your own implementation of a SessionStore
which stores the session information
as an encrypted cookie on the Client.
In order to protected against CSRF attacks it is good practice to protect HTML forms with a CSRF token. We need this for our logout form that we’ll see later.
To do this we configure a CSRFHandler
and add it to our Router
:
Keycloak Setup via OpenID Connect Discovery
Our app is registered as a confidential OpenID Connect client with Authorization Code Flow in Keycloak,
thus we need to configure client_id
and client_secret
. Confidential clients are typically used
for server-side web applications, where one can securely store the client_secret
. You can find out more
aboutThe different Client Access Types in the Keycloak documentation.
Since we don’t want to configure things like OAuth2 / OpenID Connect Endpoints ourselves, we use Keycloak’s OpenID Connect discovery endpoint to infer the necessary Oauth2 / OpenID Connect endpoint URLs.
Route handlers
We configure our route handlers via configureRoutes
:
The index handler exposes an unprotected resource:
Extract User Information from the OpenID Connect ID Token
Our app exposes a simple greeting page which shows some information about the user and provides links to other pages.
The user greeting handler is protected by the Keycloak OAuth2 / OpenID Connect integration. To show information about
the current user, we first need to call the ctx.user()
method to get an user object we can work with.
To access the OAuth2 token information, we need to cast it to OAuth2TokenImpl
.
We can extract the user information like the username from the IDToken
exposed by the user object via user.idToken().getString("preferred_username")
.
Note, there are many more claims like (name, email, givenanme, familyname etc.) available. The OpenID Connect Core Specification contains a list of available claims.
We also generate a list with links to the other pages which are supported:
The user page handler shows information about the current user:
Authorization: Checking for Required Roles
Our app exposes a simple admin page which shows some information for admins, which should only be visible for admins. Thus we require that users must have the admin
realm role in Keycloak to be able to access the admin page.
This is done via a call to user.isAuthorized("realm:admin", cb)
. The handler function cb
exposes
the result of the authorization check via the AsyncResult<Boolean> res
. If the current user has the
admin
role then the result is true
otherwise false
:
Call Services protected with Bearer Token
Often we need to call other services from our web app that are protected via Bearer Authentication. This means
that we need a valid access token
to access a resource provided on another server.
To demonstrate this we use Keycloak’s /userinfo
endpoint as a straw man to demonstrate backend calls with a bearer token.
We can obtain the current valid access token
via user.opaqueAccessToken()
.
Since we use a WebClient
to call the protected endpoint, we need to pass the access token
via the Authorization
header by calling bearerTokenAuthentication(user.opaqueAccessToken())
in the current HttpRequest
object:
Handle logout
Now that we got a working SSO login with authorization, it would be great if we would allow users to logout again.
To do this we can leverage the built-in OpenID Connect logout functionality which can be called via oAuth2Token.logout(cb)
.
The handler function cb
exposes the result of the logout action via the AsyncResult<Void> res
.
If the logout was successfull we destory our session via ctx.session().destroy()
and redirect the user to the index page.
The logout form is generated via the createLogoutForm
method.
As mentioned earlier, we need to protect our logout form with a CSRF token to prevent CSRF attacks.
Note: If we had endpoints that would accept data sent to the server, then we’d need to guard those endpoints with an CSRF token as well.
We need to obtain the generated CSRFToken
and render it into a hidden form input field that’s transfered via HTTP POST when the logout form is submitted:
Some additional plumbing:
More examples
This concludes the Keycloak integration example.
Check out the complete example in keycloak-vertx Examples Repo.
Thank you for your time, stay tuned for more updates! If you want to learn more about Keycloak, feel free to reach out to me. You can find me via thomasdarimont on twitter.
Happy Hacking!