In this demo, we use the existing public backend API called Conference API with conference sessions, speakers, and topics information. 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 successfully complete the tutorial.
- 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 Okta dashboard, and request a token that contains the custom claim called
speakerId
.
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
}
}'
Next, we set up 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 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.
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.
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%2Fptop.only.wip.la%3A443%2Fhttps%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 ID Token.
https://conferenceapi.azurewebsites.net/#id_token={TOKEN_WILL_BE_HERE}
Note that the process for retrieving a token can be different with other identity providers.
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"
}
],