Dynamic routing based on JWT Claim with Apache APISIX and Okta
Dynamic routing is a powerful feature of most modern API Gateways that allows you to route incoming requests in real time to different backend services based on various criteria such as HTTP headers, query parameters, or even the request body.
By leveraging the existing built-in plugins of Apache APISIX, developers also can create dynamic routing rules that are based on various user credentials such as access tokens, API keys, or user IDs. In this article, we'll explore the benefits of adopting dynamic routing based on authentication attributes with Apache APISIX and show you an example configuration of how to dynamically route client requests to the responsible backend services based on the JWT token's claim.
Learning objectives 📖
You will learn the following throughout the article:
- Dynamically routing traffic with API Gateway.
- Why do we need Dynamic routing based on user credentials?
- JWT Token’s claim-based dynamic routing with Apache APISIX.
API Gateway: Dynamically routing traffic
Dynamically routing traffic with the API Gateway can be used in a wide range of applications and scenarios to optimize performance, improve security, and ensure that users have access to the appropriate resources.
By dynamically routing traffic, a system can balance the load between different servers or services. It can help to ensure high availability by routing traffic to available services or servers. If one service or server fails, traffic can be automatically rerouted to another available service or server.
Dynamic routing can also be used to route traffic based on the geolocation of the user. This can help to ensure that users are connected to the closest server or service, improving response times and reducing latency.
API Gateway: User Identity-Based Dynamic Routing
Oftentimes, we want to route traffic to specific services, or paths or show only data related to the user based on user-provided identity. For example, in multi-tenant applications, different tenants may have access to different services or resources. In this case, API Gateway can route the traffic only to the appropriate tenant resources based on user credentials. Or in mobile applications, it can route traffic to specific services based on the type of device or operating system.
One of the common approaches is to use JWT tokens to authenticate and authorize requests to APIs. This means that we can create complex routing rules with API Gateway that take into account the claims present in the JWT token and uses this information to decide where to forward the request or what data to show. This approach is particularly useful when you have multiple users in the system that require different levels of access control.
Demo: JWT Token’s claim-based dynamic routing
In this demo, we use the existing public backend API called Conference API with conference sessions, speakers, and topics information. In reality, it can be your backend service. Let’s assume that we want to filter and retrieve only sessions belonging to a specific speaker who is logged into the system using its credentials such as a JWT token. For example, https://conferenceapi.azurewebsites.net/speaker/1/sessions
the request shows only sessions of a speaker with a unique id and this unique id comes from the JWT token claim as a part of its payload. Look at the below decoded token payload structure, there is a speakerId
field also included:
In this scenario, we send requests to the same Route at API Gateway and it computes the dynamic URI from the authorization header and forwards the request to the URI (See below diagram to understand the flow). To do so, we are going to implement a dynamic routing at the Apache APISIX API Gateway level based on the JWT token's claim through the use of the following plugins:
- openid-connect plugin that interacts with the identity provider(IdP) and can intercept unauthenticated requests in time to back-end applications. As an identity provider, we use the Okta that issues a JWT token with our custom claim and validates the JWT token. Or you can use other IdPs such as Keycloak, and Ory Hydra, or you can even use jwt-plugin to create a JWT token, and authenticate and authorize requests.
- serverless-pre-function plugin to write a custom Lua function code that intercepts the request, decodes, parses a JWT token claim and stores the value of the claim in a new custom header to further make authorization decisions.
-
proxy-rewrite plugin, once we have the claim in the header, we use this plugin as the request forwarding mechanism to determine which URI path needs to be used for retrieving speaker-specific sessions based on the Nginx header variable in our case it is
speakerId
that dynamically changes to create different paths/speaker/$http_speakerId/sessions
. The plugin will forward the request to the related resource in the Conference API.
Once we understood what we are going to cover throughout the demo, let’s check the prerequisites to get started with configuring the above scenario and complete the tutorial.
Prerequisites
- Docker is used to installing the containerized etcd and APISIX.
- curl is used to send requests to APISIX for configuring route, upstream, and plugin configs. You can also use easy tools such as Postman to interact with the API.
- Apache APISIX is installed in your target environment. APISIX can be easily installed and started with the following quick start guide.
- Make sure that your OKTA account is created, you registered a new app (You can follow this guide Configuring Okta), add a custom claim to a token using the Okta dashboard, and request a token that contains the custom claim called
speakerId
.
Configure the backend service (upstream)
You will need to configure the backend service for Conference API that you want to route requests to. This can be done by adding an upstream server in the Apache APISIX through the Admin API.
curl "http://127.0.0.1:9180/apisix/admin/upstreams/1" -X PUT -d '
{
"name": "Conferences API upstream",
"desc": "Register Conferences API as the upstream",
"type": "roundrobin",
"scheme": "https",
"nodes": {
"conferenceapi.azurewebsites.net:443": 1
}
}'
Create a Plugin Config
Next, we set up a new plugin config object. We will use 3 plugins openid-connect, serverless-pre-function and proxy-rewrite respectively as we discussed use cases of each plugin earlier. You need replace only openid-connect
plugin attributes (ClienID, Secret, Discovery and Introspection endpoints) with your own Okta details before you execute the curl command.
curl "http://127.0.0.1:9180/apisix/admin/plugin_configs/1" -X PUT -d '
{
"plugins": {
"openid-connect":{
"client_id":"{YOUR_OKTA_CLIENT_ID}",
"client_secret":"{YOUR_OKTA_CLIENT_SECRET}",
"discovery":"https://{YOUR_OKTA_ISSUER}/oauth2/default/.well-known/openid-configuration",
"scope":"openid",
"bearer_only":true,
"realm":"master",
"introspection_endpoint_auth_method":"https://{YOUR_OKTA_ISSUER}/oauth2/v1/introspect",
"redirect_uri":"https://conferenceapi.azurewebsites.net/"
},
"proxy-rewrite": {
"uri": "/speaker/$http_speakerId/sessions",
"host":"conferenceapi.azurewebsites.net"
},
"serverless-pre-function": {
"phase": "rewrite",
"functions" : ["return function(conf, ctx)
-- Import neccessary libraries
local core = require(\"apisix.core\")
local jwt = require(\"resty.jwt\")
-- Retrieve the JWT token from the Authorization header
local jwt_token = core.request.header(ctx, \"Authorization\")
if jwt_token ~= nil then
-- Remove the Bearer prefix from the JWT token
local _, _, jwt_token_only = string.find(jwt_token, \"Bearer%s+(.+)\")
if jwt_token_only ~= nil then
-- Decode the JWT token
local jwt_obj = jwt:load_jwt(jwt_token_only)
if jwt_obj.valid then
-- Retrieve the value of the speakerId claim from the JWT token
local speakerId_claim_value = jwt_obj.payload.speakerId
-- Store the speakerId claim value in the header variable
core.request.set_header(ctx, \"speakerId\", speakerId_claim_value)
end
end
end
end
"]}
}
}'
In the above config, the hardest part to understand can be the custom function code we wrote in Lua inside serverless-pre-function
plugin:
return function(conf, ctx)
-- Import neccessary libraries
local core = require(\"apisix.core\")
local jwt = require(\"resty.jwt\")
-- Retrieve the JWT token from the Authorization header
local jwt_token = core.request.header(ctx, \"Authorization\")
if jwt_token ~= nil then
-- Remove the Bearer prefix from the JWT token
local _, _, jwt_token_only = string.find(jwt_token, \"Bearer%s+(.+)\")
if jwt_token_only ~= nil then
-- Decode the JWT token
local jwt_obj = jwt:load_jwt(jwt_token_only)
if jwt_obj.valid then
-- Retrieve the value of the speakerId claim from the JWT token
local speakerId_claim_value = jwt_obj.payload.speakerId
-- Store the speakerId claim value in the header variable
core.request.set_header(ctx, \"speakerId\", speakerId_claim_value)
end
end
end
end
Basically, this plugin will be executed before the other two plugins and it does the following:
- Retrieves the JWT token from the Authorization header.
- Removes the "Bearer " prefix from the JWT token.
- Decodes the JWT token using the resty.jwt library.
- Retrieves the value of the "speakerId" claim from the decoded JWT token.
- Finally, it stores the value of the "speakerId" claim in the speakerId header variable.
Configure a new Route
This step involves setting up a new route that uses the plugin config, and configuring the route to work with the upstream (by referencing their IDs) we created in the previous steps:
curl "http://127.0.0.1:9180/apisix/admin/routes/1" -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"name":"Conferences API speaker sessions route",
"desc":"Create a new route in APISIX for the Conferences API speaker sessions",
"methods": ["GET"],
"uri": "/sessions",
"upstream_id":"1",
"plugin_config_id":1
}'
In the above configuration, we defined the route matching rules such as only HTTP GET requests to URI /sessions
will be routed to the correct backend service.
Obtain a token from Okta
After configuring the upstream, plugins and route on the APISIX side, now we request a token from Okta that contains our speakerId
custom claim. You can follow the guide that includes information on building a URL to request a token with Okta or simply use the below-resulting URL with your Okta issuer and client id:
https://{YOUR_OKTA_ISSUER}/oauth2/default/v1/authorize?client_id={YOUR_OKTA_CLIENT_ID}
&response_type=id_token
&scope=openid
&redirect_uri=https%3A%2F%2Fconferenceapi.azurewebsites.net
&state=myState
&nonce=myNonceValue
After you paste the request into your browser, the browser is redirected to the sign-in page for your Okta and generates an ID Token.
https://conferenceapi.azurewebsites.net/#id_token={TOKEN_WILL_BE_HERE}
Note that the process for retrieving a token can be different from other identity providers.
To check the returned ID token, you can copy the value and paste it into any JWT decoder (for example, https://token.dev).
Test the dynamic routing
Finally, now we can verify that the request is being routed to the correct URI path (with speaker-specific sessions) based on the matching criteria and JWT token claim by running another simple curl command:
curl -i -X "GET [http://127.0.0.1:9080/sessions](http://127.0.0.1:9080/sessions)" -H "Authorization: Bearer {YOUR_OKTA_JWT_TOKEN}"
Here we go, the outcome as we expected. If we set speakerId to 1 in the Okta JWT claim, Apisix routed the request to the relevant URI path and returned all sessions of this speaker in the response.
{
"collection": {
"version": "1.0",
"links": [],
"items": [
{
"href": "https://conferenceapi.azurewebsites.net/session/114",
"data": [
{
"name": "Title",
"value": "\r\n\t\t\tIntroduction to Windows Azure Part I\r\n\t\t"
},
{
"name": "Timeslot",
"value": "04 December 2013 13:40 - 14:40"
},
{
"name": "Speaker",
"value": "Scott Guthrie"
}
],
"links": [
{
"rel": "http://tavis.net/rels/speaker",
"href": "https://conferenceapi.azurewebsites.net/speaker/1"
},
{
"rel": "http://tavis.net/rels/topics",
"href": "https://conferenceapi.azurewebsites.net/session/114/topics"
}
]
},
{
"href": "https://conferenceapi.azurewebsites.net/session/121",
"data": [
{
"name": "Title",
"value": "\r\n\t\t\tIntroduction to Windows Azure Part II\r\n\t\t"
},
{
"name": "Timeslot",
"value": "04 December 2013 15:00 - 16:00"
},
{
"name": "Speaker",
"value": "Scott Guthrie"
}
],
.....
Takeaways
- With API Gateway, you can route traffic to different backend services based on various criteria.
- Dynamic routing can be achieved depending on user attributes specified in the request header, query, or body.
- You can create complex routing rules that take into account the claims present in the JWT token, and ensure that only authorized requests are allowed to access your API.
Related resources
Recommended content
Community
🙋 Join the Apache APISIX Community
🐦 Follow us on Twitter
📝 Find us on Slack
💁 How to contribute page
About the author
Visit my blog: www.iambobur.com