JWT token validation in Ktor with Client Credentials Flow
JWT token validation in Ktor with Client Credentials Flow
At Lunatech we build many tools that facilitate internal processes like keeping track of employees' vacations, share internal events and workshops and even one just for the days Lunatech offers us lunch ;) So, it's only natural that we end up creating integrations between these applications.
The goal of this post is to explore the scenario where one application authenticates with another by using the Client Credentials Flow, in Kotlin
, Ktor
and Keycloak
. To achieve this, we are going to build a simple personal luna-vacation-bot that will inform you daily, via a Slack
message, whether the present day is a vacation day or not. For this, we will need to implement a dummy luna-vacation-api as well.
In the above diagram, we can see that the starting point of our system will be the luna-vacation-bot. It asks the luna-vacation-api whether the present day is a working day or not, but, for that request to be successful, first it needs to request a JWT token from Keycloak. Once it receives the token then it can send the request to the luna-vacation-api and, based on the response of that request, it can inform the user via a Slack message. If luna-vacation-bot fails to receive a token from Keycloak or, if luna-vacation-api does not respond successfully, we can also send an error message via Slack, informing the user of the issue.
How to setup Keycloak?
First, we need a running instance of Keycloak. Recently, Lunatech deployed its own Keycloak instance for the Identity and Access Management of applications, but for this example I'm going to run a local instance of Keycloak, instead. We can easily run one using Docker
. The admin console can be found at localhost:8080
with both username and password being admin.
Create a realm
On top of the default master realm let's create our own realm called lunarealm.
Create a client
Let's create the client luna-vacation-bot which is the one that is going to need to authenticate with the luna-vacation-api and use the API:
After setting the client id, name and description, click Next for the capability config section and turn Client authentication and Service accounts roles on and all the other authentication flows off . The client is now configured for the Client Credentials Flow in OAuth2. Click Next again and Save.
In order to increase our client security, we are also creating a new Client Scope called luna-vacation-bot-scope using the default settings:
Once you save the new scope, a couple of configuration tabs will show up: Mappers and Scope. Go to Mappers and choose Configure a new mapper and then choose Audience:
Give it a name and then set luna-vacation-bot in Included Client Audience and also add it to the tokens:
Go back to the details of the luna-vacation-bot client, to Client scopes, Add client scope and choose the new luna-vacation-bot-scope Default scope and add it:
One more client scope needs a final configuration. Go to the dedicated client scope called luna-vacation-bot-dedicated and click on it. In Mappers choose Add mapper > From predefined mappers and then add audience resolve:
Finally, still inside the same luna-vacation-bot-dedicated scope, go to the Scope tag and set Full scope allowed off.
We are all set to start using JWT tokens!
Test the creation of a token
To test if luna-vacation-bot can successfully get a JWT token we also need the client secret. The secret can be found in the Credentials tab:
Let's use curl to generate the token:
curl -X post 'http://localhost:8080/realms/lunarealm/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=luna-vacation-bot' \
--data-urlencode 'client_secret=<replace-by-secret>'
The token will look something like:
{"access_token":"eyJhbG...75Eg","expires_in":300,"refresh_expires_in":0,"token_type":"Bearer","not-before-policy":0,"scope":"email luna-vacation-bot_scope profile"}
Verify the token in jwt.io
In jwt.io
you can inspect the contents of a token. Copy the access_token string and paste it in the Encoded box. I'm not going into details about the format and content of the token, you can read a nice introduction
about it in jwt.io site as well.
We can confirm that the issuer (iss) of the token is lunarealm and that the audience (aud) and the authorized party (azp) are luna-vacation-bot.
Create a Slack app
You can create your own workspace at slack.com
. Once you have it, you can create a new App
. Choose Create New App and then From scratch:
I named the app ImIOnVacation and added it to my personal workspace:
To finalize the App creation you need to set some permissions:
In Basic Information you can personalize the appearance off the app, but for now let's jump to OAuth & Permission and, from there, scroll down to the Scopes section. In User Token Scopes add the scopes chat:write:
Then scroll up and click Install to Workspace. That will take us to a permissions screen: click Allow. We can see that a User OAuth Token was created as well.
We will need this User OAuth Token to be able to send messages to Slack via this App.
Put it all together
We can now send that luna-vacation-api response to a Slack channel. The channel I'll use is the chat with myself. On Slack, channel IDs can be seen on the channel or chat details, at the very bottom.
For the communication with Slack we will use Slack's Bolt SDK , starting by adding the necessary dependencies:
implementation("com.slack.api:bolt:1.29.2")
implementation("com.slack.api:bolt-servlet:1.29.2")
implementation("com.slack.api:bolt-jetty:1.29.2")
implementation("org.slf4j:slf4j-simple:1.7.36")
Now let's compose our message
. The function getOnVacation is wrapping the luna-vacation-api request.
val text = getOnVacation(client)
val response = slack.methods("xoxp-...").chatPostMessage { req: ChatPostMessageRequestBuilder ->
req
.channel("<replace by my channel>")
.text(text)
}
Make sure luna-vacation-api is still running and now let's run luna-vacation-bot. If we check Slack we should see a message:
The complete source code for the luna-vacation-api and luna-vacation-bot are available on GitHub.
Next steps
We can transform this kind of application integration in a real automation by running it periodically. A cron job allows to achieve this goal and the quartz-scheduler library integrates well with Kotlin. The cron job can be configured as often was one wishes like several time a day or once a month, for example.
In this blog post we used a dummy API. For a real use case we could replace it with an API that provides data reports, for example. Imagine yourself surprising your manager with some automated reports delivered to their Slack or email address!