Secure Your API With JWT: Kong OpenID Connect
In this article, walk through the issues of session-based authorization and the benefits of stateless tokens, namely JWT.
Join the DZone community and get the full member experience.
Join For FreeGood Old History: Sessions
Back in the old days, we used to secure web applications with sessions. The concept was straightforward: upon user authentication, the application would issue a session identifier, which the user would subsequently present in each subsequent call. On the backend side, the common approach was to have application memory storage to handle user authorization - simple mapping between session ID and user privileges.
Unfortunately, the simple solution had scaling limitations. If we needed to scale an application server, we used to apply session stickiness on the exposed load balancer:
Or, move session data to shared storage like a database.
That caused other challenges to tackle: how to evenly distribute traffic for long living sessions and how to reduce request processing time for communication with shared session storage.
Distributed Nature of Authorization
The stateful nature of sessions becomes even more troublesome when we consider distributed applications. Handling proper session stickiness, and connection draining on a scale like multiple microservices gives no easy manageable solution.
Stateless Authorization: JWT
Luckily, we can use a stateless solution - JWT - which is based on a compact and self-contained, encoded JSON object acting as a replacement for session ID for client/server communication. The idea is to encode user privileges or roles into a token and sign data with a trusted issuer to prove token integrity. In this scenario, the user, once authenticated, gets an access token in response with all data required for authorization - no more server session storage needed. The server during authorization needs to decode the token and get user privileges from the token itself.
Exposing Unprotected API in Kong
To see how things can work, let’s use Kong, acting as an API Gateway for calling upstream service. For this demo, we will use the Kong Enterprise edition together with the OpenID Connect plugin handling JWT validation. But let's first expose some REST resources with Kong.
To make the demo simple, we can expose a single/mock endpoint in Kong which will proxy requests to the httpbin.org
service. Endpoint deployment can be done with a declarative approach: we define the configuration for setting up the Kong service that will call upstream. Then the decK tool will create respective resources in Kong Gateway. The configuration file is as follows:
_format_version: "3.0"
_transform: true
services:
- host: httpbin.org
name: example_service
routes:
- name: example_route
paths:
- /mock
Once deployed, we can verify endpoint details in Kong Manager UI:
For now, the endpoint is not protected and we can call it without any authorization details. Kong Gateway is exposed on a local machine on port 8000, so we can call it like this:
➜ ~ curl http://localhost:8000/mock/anything
{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Host": "httpbin.org",
"User-Agent": "curl/8.4.0",
"X-Amzn-Trace-Id": "Root=1-65e2f62e-2ea7165246c573e24a3efeaf",
"X-Forwarded-Host": "localhost",
"X-Forwarded-Path": "/mock/anything",
"X-Forwarded-Prefix": "/mock",
"X-Kong-Request-Id": "3cef792ded0dfb53575cd866c20aba42"
},
"json": null,
"method": "GET",
"url": "http://localhost/anything"
}
Securing API With OpenID Connect Plugin
To secure our API we need two things:
- IdP server, which will issue JWT tokens
- Kong endpoint configuration that will validate JWT tokens
Setting up an IdP server is out of scope for this blog post, but for the demo, we can use Keycloak. In my test setup, I created a “test” user which is granted to have a “custom-api-get” scope - we will use this scope name later on for authorization with Kong. To get a JWT token, we need to call the Keycloak token endpoint. It returns an encoded token, which we can decode on the jwt.io website:
On the Kong side, we will define endpoint authorization with the OpenID Connect plugin. For this, again, we will use the decK tool to update the endpoint definition.
_format_version: "3.0"
_transform: true
services:
- host: httpbin.org
name: example_service
routes:
- name: example_route
paths:
- /mock
plugins:
- name: openid-connect
enabled: true
config:
display_errors: true
scopes_claim:
- scope
bearer_token_param_type:
- header
issuer: http://keycloak:8080/auth/realms/master/.well-known/openid-configuration
scopes_required:
- custom-api-get
auth_methods:
- bearer
In the setup above, we stated that the user is allowed to call the endpoint if the JWT token contains the “custom-api-get
” scope. We also specified how we want to pass the token (header value). To enable JWT signature verification, we also had to define the issuer. Kong will use this endpoint internally to get a list of public keys that can be used to check token integrity/signature (the content of that response is cached in Kong to avoid future requests).
With this configuration, calling an endpoint without a token is not allowed. The plugin returns error details as follows:
➜ ~ curl http://localhost:8000/mock/anything
{"message":"Unauthorized (no suitable authorization credentials were provided)"}
To make it work, we need to pass a JWT token (for the sake of space, the token value is not presented):
➜ ~ curl http://localhost:8000/mock/anything --header "Authorization: Bearer $TOKEN"
{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Authorization": "Bearer $TOKEN",
"Host": "httpbin.org",
"User-Agent": "curl/8.4.0",
"X-Amzn-Trace-Id": "Root=1-65e30053-4f1b17b771c240463a878c41",
"X-Forwarded-Host": "localhost",
"X-Forwarded-Path": "/mock/anything",
"X-Forwarded-Prefix": "/mock",
"X-Kong-Request-Id": "c1cf555ab43d951f73f72a30d5546516"
},
"json": null,
"method": "GET",
"url": "http://localhost/anything"
}
We should remember that tokens have a limited lifetime (in our demo, it was 1 minute), and the plugin verifies it as well. Calling the endpoint with an expired token returns the error:
curl http://localhost:8000/mock/anything --header "Authorization: Bearer $TOKEN"
{"message":"Unauthorized (invalid exp claim (1709375597) was specified for access token)"}
Summary
In this short post, we walked through the issues of session-based authorization and the benefits of stateless tokens, namely JWT. In a microservices solution, we can move authorization from microservice implementation into a centralized layer like Gateway. We just scratched the surface of JWT-based authorization, but we can implement more advanced scenarios by validating additional claims. If you’re interested in JWT details, I recommend you familiarize yourself with the specifications. Practice will make you an expert!
Opinions expressed by DZone contributors are their own.
Comments