/images/resources/tutorials/application/tutorials-azure.jpg

Microsoft Azure API Management

On this page

This how-to describes how to set up an Azure API Management instance to act as a proxy between a mobile or web app and an API and how to integrate it with the Curity Identity Server leveraging the Phantom Token approach.

Introduction

API Integration Overview
  1. The Mobile or Web application requests an access token (AT) from the Curity Identity Server.
  2. The Curity Identity Server responds with the access token.
  3. The application requests access to the remote resource (e.g. API A)
  4. The API proxy intercepts the request and extracts the access token from the authorization header. It checks its cache to identify if this access token was already found active previously.
  5. In case the access token was not verified earlier, it sends a request to the introspection endpoint.
  6. The Curity Identity Server replies to this request according to RFC 7662.
  7. There are two possible outcomes depending on if the token is active or not:
      7a. When the access token is not active, the API proxy replies to the application with status code 401 Unauthorized
      7b. When the access token is active the API proxy forwards the request to the API, after it replaces the access token with the JWT which was in the body of the introspection endpoint’s reply (6).
  8. After validating the JWT token using the Curity Identity Server's public key, the API has access to information about the user and handles the request and responds accordingly.
  9. The API proxy forwards the API’s response back to the application.

Note

This is a simple flow, where some error cases are not considered. For example if, the original request (3) does not have an authorization header, the API proxy has to reply with 401 Unauthorized. Moreover, the API must respond accordingly when receiving an invalid or expired JWT.

Setting up the Environment

You must have at least two OAuth apps enabled to follow the flow from the Introduction. One which the Mobile or Web app will use to receive an access token and one which the API proxy, in this case Azure API Management, will use for introspection.

Introspection Procedure

Configure the Curity Identity Server to return a signed JWT the introspection response. You can read about this approach in detail in the article Introspection and Phantom Tokens.

application/jwt header

Alternatively you can use the application/jwt approach from the Introspect With Application/JWT as Accept Header section.

In the Curity Identity Server admin portal, go to SystemProcedures from the menu on the left. Add a new Token Procedure and configure it to include the JWT access token in the introspection response.

Introspect Procedure

This code is an example of an introspection procedure that returns a JWT:

javascript
123456789101112131415161718192021
/**
* @param {se.curity.identityserver.procedures.context.IntrospectionTokenProcedureContext} context
*/
function result(context) {
var responseData = {
active: context.presentedToken.active
};
if (context.presentedToken.active) {
appendObjectTo(context.presentedToken.data, responseData);
responseData.token_type = context.presentedToken.type;
responseData.client_id = context.presentedToken.delegation.clientId;
responseData.expired_scope = context.presentedToken.expiredScopes;
var defaultAtJwtIssuer = context.getDefaultAccessTokenJwtIssuer();
responseData.phantom_token = defaultAtJwtIssuer.issue(context.presentedToken.data,
context.delegation);
}
return responseData;
}

Creating an OAuth Client for Introspection

Create an OAuth client and enable the Introspection capability and set a Secret for authentication.

  1. Navigate to SystemToken ServiceClient Settings. Make sure that Introspection is enabled.
  2. Crate a client by navigating to SystemToken ServiceClients and click + New Client
  3. Give the client an ID (i.e. introspection-client) and make it a Regular Client
  4. Click Capabilities and select Introspection, click Next
  5. Select Secret from the Authentication Method drop-down list.
  6. Enter a Secret (you need this secret later when creating the policy for Azure API Management)
  7. Click Next and commit the changes
Introspection App Capabilities

Enabling the Introspection Endpoint

In the same Token Service profile, select Endpoints from the menu on the left and add an endpoint of type oauth-introspect if it doesn't already exist. Make sure that the Introspect procedure uses the newly created procedure, introspect-procedure in the example.

Introspection Endpoint

Setting up Microsoft Azure API Management

After creating an Azure account, log in to the portal and create a service instance of Azure API Management.

Create API Management Instance

Now import or create an API. As soon as the instance has started, you can configure your APIs in the API Management service for the service instance you just created. Click APIs from the menu and add or import your API. The Echo API is created when you create the API Management instance.

Add new API

Click Add policy to add a policy to the Inbound processing stage of the API. Depending on other requirements the policy can also be added to the Outbound processing stage.

Add policy

Configure a policy that checks for an authorization header, introspects the access token and forwards the request to the API. As part of this process replace the access token with a JWT received from the Curity Identity Server during the introspection call.

API Policy

This section is based on the Send-Request Microsoft API Management policy guide. The policy described below does not reply with a 401 Unauthorized only when the token is not active (according to RFC 7662). It also does so when the authorization header is missing. Moreover, it caches the association of the access token with the JWT received from the introspection response.

Extracting the Token

First the policy extracts the token from the authorization header and saves it in the context variable token.

Extract access token from authorization header:

xml
1
<set-variable name="token" value="@(context.Request.Headers.GetValueOrDefault("Authorization","scheme param").Split("").Last())" />

Lookup Cache for Existing Key

Next the policy performs a lookup in the cache to fetch a cached introspection response, if it exists.

Lookup cache for AT-JWT association:

xml
1
<cache-lookup-value key="@((string)context.Variables["token"])" variable-name="introspectToken" />

Key was Cached in Previous Request

If a cached response exists, the policy replaces the access token in the incoming request and forwards the request to the API with the JWT access token in the authorization header. The cached responses are always for active tokens, so they should contain the JWT in their body.

Forward the request while replacing AT with JWT:

xml
123
<set-header name="Authorization" exists-action="override">
<value>@($"Bearer {((IResponse)context.Variables["introspectToken"]).Body.As<JObject>()["jwt"]}")</value>
</set-header>

Make the Validation Request

If no cache result is available, the policy needs to call the introspection endpoint to validate the incoming token and to obtain the JWT.. In line 5 of the following code snippet, the value Y2xpZW5kX2lkOmNsaWVudF9zZWNyZXQ= is a base64 encoding of client_id:client_secret, of the introspection client.

Send a request to the introspection endpoint, when a previous response is not cached:

xml
123456789101111
<send-request mode="new" response-variable-name="tokenstate" timeout="20" ignore-error="true">
<set-url>https://login.example.com/oauth/v2/oauth-introspect</set-url>
<set-method>POST</set-method>
<set-header name="Authorization" exists-action="override">
<value>Basic Y2xpZW5kX2lkOmNsaWVudF9zZWNyZXQ=</value>
</set-header>
<set-header name="Content-Type" exists-action="override">
<value>application/x-www-form-urlencoded</value>
</set-header>
<set-body>@($"token={(string)context.Variables["token"]}")</set-body>
</send-request>

Store the Response

The following snippet caches the introspection response, using the access token as a key. It derives the duration of the cache from the Cache-control header of the response.

Cache the response from introspection endpoint:

xml
1234567
<cache-store-value key="@((string)context.Variables["token"])"
value="@(((IResponse)context.Variables["tokenstate"]))"
duration="@{
var header = ((IResponse)context.Variables["tokenstate"]).Headers.GetValueOrDefault("Cache-Control","");
var maxAge = Regex.Match(header, @"max-age=(?<maxAge>\d+)").Groups["maxAge"]?.Value;
return (!string.IsNullOrEmpty(maxAge))?int.Parse(maxAge):300;
}" />

Check the Response

The policy parses the introspection response (by accessing the cached value). According to the active status the policy will either respond with 401 Unauthorized or forward the request to the API after replacing the access token with the JWT from the introspection response.

Continue or block the request according to the introspection status:

xml
12345678910111213141516171819
<choose>
<!--Check active property in response -->
<when condition="@((bool)((IResponse)context.Variables["introspectToken"]).Body.As<JObject>()["active"] == false)">
<!--Return 401 Unauthorized with http-problem payload -->
<return-response response-variable-name="responseVariableName">
<set-status code="401" reason="Unauthorized" />
<set-header name="WWW-Authenticate" exists-action="override">
<value>Bearer, error="invalid_token"</value>
</set-header>
</return-response>
</when>
<otherwise>
<!-- Response contains active=true, replace AT with JWT -->
<cache-lookup-value key="@((string)context.Variables["token"])" variable-name="introspectToken" />
<set-header name="Authorization" exists-action="override">
<value>@($"Bearer {((IResponse)context.Variables["introspectToken"]).Body.As<JObject>()["jwt"]}")</value>
</set-header>
</otherwise>
</choose>

Note

You can find the complete policy in Appendix A: – Require OAuth Token Policy

Introspect With Application/JWT as Accept Header

The Curity Identity Server can also respond to requests in the introspection endpoint with the Accept: application/jwt header. When introspecting a valid access token, the Curity Identity Server responds with 200 OK and the JWT in the body of the response. An expired or invalid access token, causes the Curity Identity Server to respond with 204 No Content. This means that the gateway doesn’t need to parse the JSON (as when using normal introspection), making the proxying even faster. Appendix B: – Application/JWT Policy contains a policy fragment that uses application/jwt, and that takes the status codes from the Curity Identity Server in to consideration so that the gateway responds accordingly.

Introspection request using application/jwt as accept header:

xml
123
<set-header name="Accept" exists-action="override">
<value>application/jwt</value>
</set-header>

Handle introspection response and different status codes:

xml
1234567891011121314151617181920212223242526272829303132333435363738394041
<choose>
<!-- When Curity responds with 200, token is valid and JWT is in the response body -->
<when condition="@((bool)(((IResponse)context.Variables["introspectToken"]).StatusCode == 200))">
<cache-lookup-value key="@((string)context.Variables["token"])" variable-name="introspectToken" />
<set-header name="Authorization" exists-action="override">
<value>@($"Bearer {((IResponse)context.Variables["introspectToken"]).Body.As<string>()}")</value>
</set-header>
</when>
<!-- When Curity responds with 204, the token is not active -->
<when condition="@((bool)(((IResponse)context.Variables["introspectToken"]).StatusCode == 204))">
<return-response response-variable-name="responseVariableName">
<set-status code="401" reason="Unauthorized" />
<set-header name="WWW-Authenticate" exists-action="override">
<value>Bearer, error="invalid_token"</value>
</set-header>
</return-response>
</when>
<!-- When Curity responds with 503, return 503 -->
<when condition="@((bool)(((IResponse)context.Variables["introspectToken"]).StatusCode == 503))">
<return-response response-variable-name="responseVariableName">
<set-status code="503" />
</return-response>
</when>
<!-- When Curity responds with 401, 403, 404,500-502, 504-599, return 502 -->
<when condition="@(
(bool)(((IResponse)context.Variables["introspectToken"]).StatusCode >= 500) ||
(bool)(((IResponse)context.Variables["introspectToken"]).StatusCode == 401) ||
(bool)(((IResponse)context.Variables["introspectToken"]).StatusCode == 403) ||
(bool)(((IResponse)context.Variables["introspectToken"]).StatusCode == 204)
)">
<return-response response-variable-name="responseVariableName">
<set-status code="502" />
</return-response>
</when>
<otherwise>
<!-- Curity responds with other response codes, return 500 -->
<return-response response-variable-name="responseVariableName">
<set-status code="500" />
</return-response>
</otherwise>
</choose>

Testing Your Integration

Azure API Management allows for an easy way to test your APIs using the Azure Portal. The Azure Portal can act as a Web application, making calls to your API, making it easy to trace and debug your API calls.

  1. Navigate to your API that the policy applies to. Make sure the API has at least one Operation that you can use to test.
  2. In the top menu click Test and choose one of the Operations.
  3. First try to send a request. By default the request does not contain an access token in the Authorization header and the response of the test should be 401 Unauthorized.
  4. Use for example OAuth Tools to obtain an access token from your Curity Identity Server instance.
  5. Add the access token to a header in the test by clicking + Add header. The name of the header needs to be Authorization. Paste the access token that you obtained in step 4 in to the Value field.
  6. Send the request again. The response should be 200 OK. If you use the Echo API you can also see that the authorization header that it echoes back contains the JWT and not the opaque access token from the original request.

Example request using the provided Echo API:

http
123
GET https://curity-pme-api.azure-api.net/echo/resource-cached?param1=sample HTTP/1.1
Host: curity-pme-api.azure-api.net
Authorization: _0XBPWQQ_cf756dc2-ab1c-4c85-9178-c69d303bb17d

The body of the response will be something like the following, with the Authorization header containing the JWT.

Body of a successful response (truncated for readability):

http
123456
HTTP/1.1 200 OK
accept: */*
accept-encoding: gzip,deflate,br,zstd
accept-language: en-US,en; q=0.5
authorization: Bearer eyJraWQiOiItNTg5NTg2NTA3IiwieDV0Ijo...OcYMRDZ8xuCoBw
referer: https://apimanagement.hosting.portal.azure.net/

An unauthorized request will return the following:

http
12345
HTTP/1.1 401 Unauthorized
content-length: 0
date: Wed, 06 Nov 2024 22:31:17 GMT
vary: Origin
www-authenticate: Bearer, error="invalid_token"

Appendix

xml
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
<policies>
<inbound>
<!-- Extract Token from authorization header parameter -->
<set-variable name="token" value="@(context.Request.Headers.GetValueOrDefault("Authorization","scheme param").Split(' ').Last())" />
<!-- Check if the token variable is empty or null and return 401 Unauthorized if that is the case -->
<choose>
<when condition="@(System.String.IsNullOrEmpty((string)context.Variables["token"]))">
<return-response response-variable-name="responseVariableName">
<set-status code="401" reason="Unauthorized" />
<set-header name="WWW-Authenticate" exists-action="override">
<value>Bearer, error="invalid_token"</value>
</set-header>
</return-response>
</when>
</choose>
<!-- Check if there is a previous value in the cache for this token -->
<cache-lookup-value key="@((string)context.Variables["token"])" variable-name="introspectToken" />
<choose>
<!-- If we don’t find it in the cache, make a request for it and store it -->
<when condition="@(!context.Variables.ContainsKey("introspectToken"))">
<!--Send request to Token Server to validate token (see RFC 7662) -->
<send-request mode="new" response-variable-name="tokenstate" timeout="20" ignore-error="true">
<set-url>https://login.example.com/oauth/v2/oauth-introspect</set-url>
<set-method>POST</set-method>
<set-header name="Authorization" exists-action="override">
<value>Basic Y2xpZW5kX2lkOmNsaWVudF9zZWNyZXQ=</value>
</set-header>
<set-header name="Content-Type" exists-action="override">
<value>application/x-www-form-urlencoded</value>
</set-header>
<set-body>@($"token={(string)context.Variables["token"]}")</set-body>
</send-request>
<!-- cache the response of the Token Server with the AT as the key -->
<cache-store-value key="@((string)context.Variables["token"])" value="@(((IResponse)context.Variables["tokenstate"]))" duration="@{
var header = ((IResponse)context.Variables["tokenstate"]).Headers.GetValueOrDefault("Cache-Control","");
var maxAge = Regex.Match(header, @"max-age=(?<maxAge>\d+)").Groups["maxAge"]?.Value;
return (!string.IsNullOrEmpty(maxAge))?int.Parse(maxAge):300;
}" />
</when>
</choose>
<!-- Query the cache for a value with key AT and store the data in a context variable "introspectToken" -->
<cache-lookup-value key="@((string)context.Variables["token"])" variable-name="introspectToken" />
<choose>
<!--Check active property in response -->
<when condition="@((bool)((IResponse)context.Variables["introspectToken"]).Body.As<JObject>()["active"] == false)">
<!-- active is equal to false, return 401 Unauthorized -->
<return-response response-variable-name="responseVariableName">
<set-status code="401" reason="Unauthorized" />
<set-header name="WWW-Authenticate" exists-action="override">
<value>Bearer, error="invalid_token"</value>
</set-header>
</return-response>
</when>
<otherwise>
<!-- Response contains active=true replace AT with JWT-->
<cache-lookup-value key="@((string)context.Variables["token"])" variable-name="introspectToken" />
<set-header name="Authorization" exists-action="override">
<value>@($"Bearer {((IResponse)context.Variables["introspectToken"]).Body.As<JObject>()["jwt"]}")</value>
</set-header>
</otherwise>
</choose>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
</policies>

Join our Newsletter

Get the latest on identity management, API Security and authentication straight to your inbox.

Start Free Trial

Try the Curity Identity Server for Free. Get up and running in 10 minutes.

Start Free Trial