Dynamic routing based on user credentials with API Gateway
Tags: webdev, tutorial, programming, api
Published: 2023-04-17 05:33:00

## 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](https://apisix.apache.org/) and show you an example configuration of **how to dynamically route client requests** to the responsible backend services based on the [JWT](https://jwt.io/) 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](https://apisix.apache.org/blog/2022/10/27/ten-use-cases-api-gateway/#api-fault-handling) 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.

![dynamically routing traffic with Apache APISIX](https://static.apiseven.com/uploads/2023/04/07/KCzNPvVG_Global%20Community%20Meeting%20slides%20%288%29.png)

Dynamic routing can also be used to route traffic [based on the geolocation](https://apisix.apache.org/blog/2022/11/09/georouting-apisix/) of the user. This can help to ensure that users are connected to the closest server or service, improving response times and reducing latency.

![geo location routing traffic with Apache APISIX](https://static.apiseven.com/uploads/2023/04/07/XiTi0a2O_Global%20Community%20Meeting%20slides%20%289%29.png)

## 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.

![Dynamic routing traffic based on JWT token with Apache APISIX](https://static.apiseven.com/uploads/2023/04/07/vMTbaoic_Global%20Community%20Meeting%20slides%20%2810%29.png)

## Demo: JWT Token’s claim-based dynamic routing

In this demo, we use the existing public backend API called [Conference API](https://conferenceapi.azurewebsites.net/?format=json) 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:

![JWT Token with a custom claim](https://static.apiseven.com/uploads/2023/04/07/nJEzkqEH_Global%20Community%20Meeting%20slides%20%2811%29.png)

In this scenario, we send requests to the same [Route](https://apisix.apache.org/docs/apisix/terminology/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:

1. [openid-connect](https://apisix.apache.org/docs/apisix/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](https://www.okta.com/) that issues a JWT token with our custom claim and validates the JWT token.  Or you can use other IdPs such as [Keycloak](https://www.keycloak.org/), and [Ory Hydra](https://www.ory.sh/hydra/), or you can even use [jwt-plugin](https://apisix.apache.org/docs/apisix/plugins/jwt-auth/) to create a JWT token, and authenticate and authorize requests.
2. [serverless-pre-function](https://apisix.apache.org/docs/apisix/plugins/serverless/) 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.
3. [proxy-rewrite](https://apisix.apache.org/docs/apisix/plugins/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](https://nginx.org/en/docs/http/ngx_http_core_module.html) 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](https://docs.docker.com/get-docker/) is used to installing the containerized etcd and APISIX.
- [curl](https://curl.se/) is used to send requests to APISIX for configuring route, upstream, and plugin configs. You can also use easy tools such as [Postman](https://www.postman.com/) 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](https://docs.api7.ai/apisix/getting-started/#get-apisix).
- Make sure that your [OKTA](https://www.okta.com/) account is created, you registered a new app (You can follow this guide [Configuring Okta](https://api7.ai/blog/how-to-use-apisix-auth-with-okta#:~:text=ready%20for%20use.-,Step%201%3A%20Configuring%20Okta,-Log%20in%20to)), a[dd a custom claim to a token](https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/main/#add-a-custom-claim-to-a-token) using the Okta dashboard, and [request a token that contains the custom claim](https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/main/#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](https://apisix.apache.org/docs/apisix/admin-api/).

```bash
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](https://apisix.apache.org/docs/apisix/terminology/plugin-config/) object. We will use 3 plugins [openid-connect](https://apisix.apache.org/docs/apisix/plugins/openid-connect/), [serverless-pre-function](https://apisix.apache.org/docs/apisix/plugins/serverless/) and [proxy-rewrite](https://apisix.apache.org/docs/apisix/plugins/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. 

```bash
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:

```lua
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:

1. Retrieves the JWT token from the Authorization header.
2. Removes the "Bearer " prefix from the JWT token.
3. Decodes the JWT token using the resty.jwt library.
4. Retrieves the value of the "speakerId" claim from the decoded JWT token.
5. 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:

```bash
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](https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/main/#request-a-token-that-contains-the-custom-claim) with Okta or simply use the below-resulting URL with your Okta issuer and client id:

```bash
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.*

```bash
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](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:

```bash
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.

```json
{
  "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

- [10 most common use cases of an API Gateway](https://apisix.apache.org/blog/2022/10/27/ten-use-cases-api-gateway/)
- [Geo-routing with Apache APISIX](https://apisix.apache.org/blog/2022/11/09/georouting-apisix/)

### Recommended content

- [Why Is Apache APISIX the Best API Gateway?](https://api7.ai/blog/why-is-apache-apisix-the-best-api-gateway)
- [How to choose the right API Gateway](https://www.iambobur.com/post/how-to-choose-the-right-api-gateway)

### Community

🙋 [Join the Apache APISIX Community](https://apisix.apache.org/docs/general/join/)
🐦 [Follow us on Twitter](https://twitter.com/ApacheAPISIX)
📝 [Find us on Slack](https://join.slack.com/t/the-asf/shared_invite/zt-vlfbf7ch-HkbNHiU_uDlcH_RvaHv9gQ)
💁 [How to contribute page](https://apisix.apache.org/docs/general/how-to-contribute/)

### About the author

Visit my blog: [www.iambobur.com](https://www.iambobur.com/)