
Implementing Dynamic User Routing
Intro
The Dynamic User Routing article describes a design pattern for routing OAuth requests for end-users to their home region within a global IAM deployment. In this tutorial and video, we will demonstrate how to get an end-to-end solution working.
Get the Code
First clone the GitHub Repository, which provides some Docker resources to enable a fast setup:
Understand Components
The example setup uses the following components within a Docker Compose network. The Curity Identity Server will be exposed to the host computer via a reverse proxy, at a base URL of http://localhost:80:
Install Prerequisites
Install Docker Desktop
First, ensure that Docker Desktop is installed and configured with more than the default 2GB of RAM to ensure the cluster has sufficient resources:
Install ngrok
OAuth.tools will be used as a test client. In order for it to connect to http://localhost:80, an ngrok tunnel will be automatically created. If you don't have ngrok installed, this can be done with a command such as brew install ngrok
.
Provide a License File
Obtain a license for the Curity Identity Server by signing up on the Developer Portal if needed. The license.json
file then must be copied into the idsvr
folder of the repository.
Run the System
View the Docker Compose File
The Docker Compose file runs the following components on a simple network to demonstrate the main ingredients needed for dynamic routing of requests:
Run All Containers
You can then run the system with one of the following commands, depending on which of these reverse proxies you prefer to use:
Reverse Proxy | Command |
---|---|
NGINX OpenResty | ./run.sh nginx |
Kong Open Source | ./run.sh kong |
The script will generate an internet URL that points to http://localhost:80 and prompt you to copy it for use in OAuth Tools later on:
After pressing the enter key
to proceed, all containers will start deploying. In addition, OAuth Tools will be opened in a browser window for testing:
Once the Docker system is initialized, select Use Webfinger
in OAuth Tools and copy in the ngrok URL to create an environment for testing:
Access the System
You can then access the system via the following URLs, though the ngrok generated value will be different in your environment:
Component | URL | Description |
---|---|---|
Admin UI | https://localhost:6749/admin | Sign in as user admin with Password1 to view the Identity Server configuration |
Exposed OAuth Endpoints | https://a072b06f91da.eu.ngrok.io/oauth/v2/oauth-anonymous/.well-known/openid-configuration | The OAuth endpoints that will be used when OAuth Tools is used as a client |
The system represents a straightforward global cluster with an admin node, a European runtime node, and an American runtime node:
Query User Account Data
A user called testuser.eu
exists in the European database, and a user called testuser.us
exists in the US database. This can be seen by first remoting to a database container:
export USERDATA_EU_CONTAINER_ID=$(docker container ls | grep data_eu | awk '{print $1}')
docker exec -it $USERDATA_EU_CONTAINER_ID bash
Then from within the container's terminal, issue database queries by pasting in these commands:
export PGPASSWORD=Password1 && psql -p 5432 -d idsvr -U postgres
select username, email from accounts;
Note that if the American user was routed to the below European database, the user would not be found and would experience a login error. The reverse proxy will prevent this from ever happening, though, due to its reliable routing:
Test Logins
Use a Test Client
In OAuth Tools run a Code Flow with the following settings, to match those supplied in the configuration of the Curity Identity Server instances:
Run Logins as EU and US Test Users
In the early stages of authentication, the user has not yet been identified. Therefore, requests are routed to the default region. In the example setup, this region is the European instance. Credentials are then collected in two stages, starting with a Username Authenticator:
Once identified, the authentication workflow continues in the user's home region so that the user can sign in reliably. Note also that the Username Password Authenticator is being used in Password Only
mode, and its user name field is automatically populated from the previous screen:
For the American user, a zone transfer action
will occur when the Username Authenticator is submitted. This ensures that credentials are verified against the US data source.
View Logs
During processing, you can run one of the following requests to view logs for the reverse proxy:
Reverse Proxy | Command |
---|---|
NGINX OpenResty | ./logs.sh nginx |
Kong Open Source | ./logs.sh kong |
Info-level log statements will be output by the reverse proxy when a zone is detected in either an HTTP Only cookie or a wrapped token:
*** Found zone 'eu' in cookie, client: 172.19.0.1, server: , request: "POST /authn/authentication/UserName-Password HTTP/1.1"
*** Found zone 'eu' in wrapped token, client: 172.19.0.1, server: , request: "POST /oauth/v2/oauth-token HTTP/1.1"
*** Found zone 'eu' in cookie, client: 172.19.0.1, server: , request: "POST /authn/authentication/UserName HTTP/1.1"
*** Found zone 'us' in cookie, client: 172.19.0.1, server: , request: "POST /authn/authentication/UserName-Password HTTP/1.1"
*** Found zone 'us' in wrapped token, client: 172.19.0.1, server: , request: "POST /oauth/v2/oauth-token HTTP/1.1"
View Wrapped Tokens
In OAuth Tools, view authorization codes, access tokens and refresh tokens
to see that these are now in JWT format and contain a zone value. Note that these values remain confidential and do not contain any sensitive user data:
{
iss: http://curity-demo.ngrok.io/oauth/v2/oauth-anonymous
azp: tools-client
jti: P$cb4d89ce-0e78-449e-b59e-58d9afd79184
iat: 1623230431
exp: 1623230731
zone: us
}
Reverse Proxy Routing
The reverse proxy's role is to read the zone from HTTP Only cookies received in front channel requests, or from wrapped tokens received in back channel requests. In both cases, the reverse proxy selects the host address from a map at runtime, which in the example setup looks like this:
Zone Value | Mapped Host Address |
---|---|
unknown | http://internal-curity-eu:8443 |
eu | http://internal-curity-eu:8443 |
us | http://internal-curity-us:8443 |
This type of routing is possible in any reverse proxy or API gateway with good support for content-based routing. It requires a small amount of code via a simple plugin. For some step-by-step instructions, see the API Gateway Guides for details on how to integrate the routing logic.
--
-- Try to read the zone value from an OAuth request
--
function _M.get_zone_value(config)
if not verify_options(config) then
return nil
end
local method = string.lower(ngx.var.request_method)
if method == 'options' or method == 'head' then
return nil
end
-- First try to find a value in the zone cookie
local zone = get_zone_from_cookie(config.cookie_name)
-- Otherwise, for POST messages look in the form body
if zone == nil and method == 'post' then
zone = get_zone_from_form(config.claim_name)
end
return zone
end
Identity Server Configuration
System Zones
Our example multi-zone system was configured by first defining zones in the System / Zones area:
Service Roles
Separate service roles were then created for each zone, and the corresponding zone value is configured under advanced properties of each service role:
Every running instance of the Curity Identity Server is given a service role when it starts, and this is configured for the example instances in the Docker Compose file.
Data Separation
A multi-zone data source is then used to map zones to individual data sources, each of which are PostgreSQL JDBC data sources in the example setup:
Enabling Wrapped Tokens
Next, navigate to Profiles / Token Service / Token Issuers
and note that the system is configured to use wrapped tokens:
Configuring the Zone Claim
A zone claim is configured to be issued in wrapped tokens, and the example setup issues it within the openid
scope. The value of this claim is provided by the System Claims Provider
, which writes the fixed value of the service role to wrapped tokens:
Authentication Actions
The tools-client
used for testing references a Username Password authenticator that uses a Username Authenticator as a prerequisite:
The Username authenticator uses two important authentication actions
to manage zones, to set the zone value for a user, and to perform a zone transfer:
Set Zone Action
Some logic needs to be provided to associate users to zones. This is done in a script action, which could be implemented in various ways. The example uses a simple option of determining the zone based on a suffix in the user ID, which might exist when using email-based identifiers.
function result(transformationContext) {
var attributes = transformationContext.attributeMap;
if (attributes.subject.endsWith('.eu')) {
attributes.zone = 'eu';
} else {
attributes.zone = 'us';
}
return attributes;
}
A more real-world implementation might instead perform a lookup on a globally replicated data source. This would be a simple map of user IDs to zones and would contain no personally identifiable information.
Zone Transfer Action
The job of the zone transfer action is simply to write a zone cookie, then trigger a redirect to the next stage of authentication processing. This mechanism allows the reverse proxy to read the cookie and re-route the next stage to a server with access to data for the user's region.
Video Tutorial
The below video provides an interactive walkthrough of the details described in this article, which you can watch to see the solution in action:
Conclusion
Dynamic User Routing with the Curity Identity Server can be implemented fairly easily in reverse proxies and API gateways. This extra layer provides a level of reliability that can be difficult to achieve via infrastructure alone.
With this foundation in place, and with wrapped access tokens sent to your APIs, the reverse proxy routing could be easily extended to perform dynamic routing of API requests. This would enable you to separate your own business data by region when required.