Tenant routing rules, claims processing, and authentication protocol translations are often needed to expose a backend to many frontends and clients. A reverse proxy hides some of this complexity for the frontends connecting and centralizing the management. Scenarios are amongst:
- Frontend leverages modern OAuth, while backend services do not. Transformation is needed for downstream calls. Like OICD/OAuth transformation to other legacy authentication protocols used downstream from a proxy.
- Token claim enrichments or creating new tokens to be used downstream - i.e., add additional claims, like membership, etc.
- The existing backend has specific tenant routing requirements like subdomains (customer1.contoso.com), query parameters, path segments, headers, or claims from bearer tokens.
- Specific handling of tenants in a multitenant backend (like special treatment for paid vs. unpaid tenants) or handling limits per tenant, per client, or other dimensions.
A reverse proxy or gateway can solve these and other challenges, as it sits between client devices and one or more backend servers, forwarding client requests to servers and then returning the server's response back to the client. The Gateway Routing pattern speaks to and addresses these challenges.
The current sample uses the YARP proxy, as the reverse proxy implementation. Services like Azure Application Gateway and Azure API Management, provide most of the same and even more functionality. However, at the time of this writing, these existing PaaS services do not offer token-based routing or claims transformation, like adding claims or mapping to other authentication protocols. YARP provides that flexibility, as the pipeline is fully extendable.
Main components
The sample consists of the following:
-
YARP proxy implementation which routes tenants to different backends based on the Azure AD tenant id. The proxy is deployed on an Azure web application.
-
A Tenant Repository (
ITenantRepository
), is used to serve information regarding tenants. For data persistence, it's using Azure App Configuration as the store for known tenants. Any data store could be used. Azure App Configuration provides benefits like versioning and restoring configurations and change events. The sample uses dynamic configuration, causing changes in App Configuration to be pushed to subscribers. -
A backend REST API, which tenants are routed to. This is the weather API but configured to authorize using the token issued from the proxy.
-
A tenant management service (API) with OpenAPI exposed. This acts as the interface to update the tenant repository. This API is not deployed with the scripts to deploy the above components.
Tenant routing
The end-to-end flow for performing a request from a client (tenant) to a designated backend involves these steps:
-
The proxy receives an authorized request.
-
The proxy inspects the bearer token and extracts the claims needed. In this case, only
http://schemas.microsoft.com/identity/claims/tenantid
claim is used. -
The poxy uses the tenant id to retrieve tenant information from the Tenant Repository. Tenant Repository contains the hosting Uri and the state of the tenant (enabled or disabled).
-
Additional permissions are read from the 'Permission Service' depending on what the downstream services need.
-
The proxy creates a new self-signed JWT and uses this security token when proxying the incoming request to the hosting location for the tenant. This could be any transformation required for calling downstream services. In this sample, the permission service is called to add additional claims to the JWT. But it does not have to be a JWT token, it should be whatever the downstream service needs, like basic auth token, API key etc.
-
The request is proxied based on information from the Tenant repository.
The sequence diagram below illustrates the flow.
Tenant management
The sample includes a simple management abstraction that allows service administrators to configure tenant settings and routing URI. The configuration storage is handled in Azure App Configuration as the external configuration store, but any other backing store could be implemented. This would typically be deployed as part of the SaaS control plane. A tenant management component/service handles scenarios like:
- Adding, removing, or blocking tenants.
- Assigning capacity, limits per tenant, or limits per hosting service.
- Reassigning tenants to a new host by changing the destination for tenants.
The high-level sequence diagram for calling the backend looks like this:
Some key considerations are that the proxy requires hosting, maintenance, and operations and that the proxy will be on the critical path. Azure Application Gateway or Azure API Management are hosted services and takes these responsibilities. Besides that, adding a proxy or a gateway has a cost and adds a processing layer and network hop, which can impact the overall performance. Networking configuration has not been included in this sample, where for most cases, deployments would be performed into separate subnets, and a web application firewall would front the proxy.
Observability
The sample is configured to send logs and metrics to Azure Application Insights. This provides the necessary operational telemetry to inspect how the proxy and downstream services perform and operate.
Note: Trace levels are configured centrally in the Azure App Configuration, in the configuration
API:Settings${environment}
. Other application insights functionality, like dependency tracing, can also be centrally enabled or disabled.
Having visibility into the end-to-end request is essential for development and production. The KQL below shows how a specific request can be traced using operation_id
.
let opid ="INSERT OPERATIOb_ID";
union requests,traces,dependencies, exceptions
| where operation_Id == opid or operation_ParentId == opid
| extend CategoryName_ = tostring(customDimensions.CategoryName)
| extend executingProcess = strcat(cloud_RoleName, '-',cloud_RoleInstance)
| project-reorder timestamp,itemType, operation_Id, executingProcess, name, message,CategoryName_, url, duration
| order by timestamp asc
Requests, traces, and dependencies are logged to Azure Application Insights. The picture above shows the request to the proxy, the traces for the proxy (Proxying to..), the dependency traces, the request to the weather API, and finally, the traced response.
Application map
The Application Map shows the communication between services. The obvious are calls from proxy and downstream to weather API. There are also a few other dependencies shown that illustrate how the service operates:
127.0.0.1:*
- looking into that revealsGET 127.0.0.1:41793/msi/token/
. This is the service getting a token for the managed identity used to connect other servicesproxyservicebus-*
is the subscription to the Service Bus topic, which nodes subscribe to, to get notified of configuration changesconfigurationclient
- calls to configuration clients are triggered once a node receives a message on the Azure Service Bus.
Building and deploying the sample
Install scripts are tested on Ubuntu on WSL. Main principle for the scripts is to store variables in config.json
once scripts run. That will result in specific deployment parameters used in Bicep to provision and configure. config.json
is also used directly by the ASP.NET core configuration provider if the sample is running locally.
-
Clone the repository - Tenant-Reverse-Proxy.
-
Go to the scripts folder
-
Optional: run
./preReqs.sh
to install needed prerequisites. For manual installation of the prerequisites:apt install jq zip azure-cli dotnet-sdk-7.0
-
run
./aadApp.sh
to create application registration in AAD. This will create a multitenant AAD app, which the proxy will use. -
run
./provision.sh
to provision Azure resources, using Bicep. -
run
./addCurrentTenantToRepository.sh
, this will create a registration that will route your tenant the weather API installed for testing routing information is stored in App Configuration underTenantDirectory:Tenants$Production
with this format:[ { "Tid": "{Id of the AAD tenant, for which the proxy will route requests}", "Destination": "https://{Replace with weather api prefix}.azurewebsites.net", "Alias": "{Tenant-Something}", "State":"Enabled" } ]
-
run
./deploy.sh
to build the solution and deploy the solution.
Setting up Postman to call the APIs
A Postman environment is generated to help setup the authentication and to make the request against the proxy.
- In the scripts folder, run the
createPostmanEnvironment.sh
script that generates an environment file and copies the sample collection. - Open Postman and import the generated files
TenantProxy-dev.postman_environment.json
andTenantProxy.postman_collection.json
- In Postman, select the imported environment to make sure the correct variables are used. The environment name contains the resource group name for the deployment.
- Get a new access token, and press "Use Token". This will use the environment which contains the client id, scope, etc.
- Call the API using the token.
- Enjoy the weather, served through the proxy by the backend API the tenant is routed to
Running locally
From the scripts folder, do the following steps:
- Ensure the above setup steps have been performed to provision resources on Azure.
- From the scripts folder run
createDevServicePrincipal.sh
. This will create a service principal with permissions identical to the managed identity. Values are stored inconfig.json
and read as part of the startup. - For startup projects, select multiple and select Proxy and WeatherApi. Proxy needs to run https on port 7001, and WeatherApi on port 9090.
- If testing with Postman, use the request stored in the localhost folder
- Ensure that the correct configuration is loaded for the Development environment by going to Azure App Configuration and editing the configuration
TenantDirectory:Tenants$Development
. If not changed, routing will be towards the cloud deployment - which is also possible.
Control Plane APIs
For interacting with the control plane, there's a simple sample that updates the Tenant Repository. It can be started locally (localhost), for test purposes, using a service principal to talk to the App Configuration service.
Above are a few operations relevant to managing tenants.
Summary
A reverse proxy works as an intermediary between clients and backends. Having a proxy or gateway in between enables inspection and rewrite of requests. For this sample, the scenario is to route tenants identified by the tenant id claim in the bearer token to the right hosting endpoint in the backend. Tenant management is implemented to hold the individual configurations of the tenants.
Resources
- YARP Documentation (microsoft.github.io)
- Dynamic configuration using App Configuration
- Backends for Frontends pattern - Azure Architecture Center | Microsoft Learn
- Gateway routing pattern
- External Configuration Store pattern - Azure Architecture Center | Microsoft Learn
Updated Jul 06, 2023
Version 1.0henrikwh
Microsoft
Joined August 05, 2022
FastTrack for Azure
Follow this blog board to get notified when there's new activity