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
- The Mobile or Web application requests an
Access Token
from Curity. - The Curity Identity Server responds with the
Access Token
. - The application requests access to the remote resource (i.e. API A)
- The API Proxy intercepts the request and extracts the
Access Token
from theAuthorization
Header. It checks its cache to identify if thisAccess Token
is already foundactive
previously. - In case the
Access Token
was not verified earlier, it sends a request to theintrospection
endpoint. - The Curity Identity Server replies to this request according to RFC 7662. The body of the response contains a JSON with a
boolean
active
property. -
- When the
Access Token
is notactive
, the API Proxy replies to the application with status code401 Unauthorized
- When the
Access Token
isactive
the API Proxy forwards the request to the API, after it replaces theAccess Token
with theJWT
which was in the body of theintrospection
endpoint’s reply (6).
- When the
- The API, after validating the
JWT
token, using the Curity’s public key, has access to information about the user, handles the request and responds accordingly. - The API proxy forwards the API’s response back to the application.
Note
This is a simple flow, where some error cases are not presented. For example if the original request (3) does not have an Authorization Header, the proxy has to reply with 401 Unauthorized. Moreover, the API must respond accordingly when receiving an invalid or expired JWT
.
Setting up the Environment
In order to be able to follow the flow described in the Introduction of this section, you must have at least two OAuth apps enabled. 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
In the Curity admin portal, go to System and select Procedures from the menu on the left. Here you can add a new Token Procedure which you should configure to return a JWT
containing information for the user.
This is needed in order for the Introspection result to also return the signed JWT. This approach is fully detailed in the article Introspection and Phantom Tokens.
An alternative option is to leverage the application/jwt
approach described in the Introspect with application/jwt as accept header section.
This code is an example of an introspection procedure that returns a JWT:
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.jwt = defaultAtJwtIssuer.issue(context.presentedToken.data,context.delegation);}return responseData;}
Creating an OAuth app for Introspection
Next, go to the OAuth profile you wish to enable introspection and create an app (i.e. introspection-client). Set up a Secret for this app (you will need this secret later when creating the policy for Azure API Management). Finally, enable the introspection capability for this client, as shown in the image below.
Enabling the Introspection Endpoint
In the same OAuth 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.
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.
The next step is to 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.
You can add a Policy to your API by clicking Add policy. You can apply a Policy to either the Inbound or Outbound processing stage of the API.
Next, we present a Policy that checks for an Authorization Header, introspects the Access Token
and forwards the request to the API after replacing the Access Token
with a JWT
received from Curity.
API Policy
This section is based on the Send-Request Microsoft API Management policy guide. The policy described below though, does not reply with a 401 Unauthorized
only when the token is not active
(according to RFC 7662), but does so when the Authorization header is missing too. Moreover, it caches the association of the Access Token
with the JWT
received from the introspection
endpoint.
Extracting the token
The first step is to extract the token from the Authorization Header and save it to the context variable token
.
Extract token from Authorization Header:
<set-variable name="token" value="@(context.Request.Headers.GetValueOrDefault("Authorization","scheme param").Split("").Last())" />
Lookup cache for existing key
Next a lookup is made in the cache to fetch a cached response from the introspection endpoint, if any.
Lookup cache for AT-JWT association:
<cache-lookup-value key="@((string)context.Variables["token"])" variable-name="introspectToken" />
Key was cached in previous request
In the case that there exists a cached response, simply forward the request to the API
after replacing the Access Token with the JWT
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:
<set-header name="Authorization" exists-action="override"><value>@($"Bearer {((IResponse)context.Variables["introspectToken"]).Body.As<JObject>()["jwt"]}")</value></set-header>
Make the validation request
In the case where no response is cached using the Access Token
as a key, we need to contact the introspection
endpoint in order to validate the Access Token
. In line 6 of the following code snippet, the value Y2xpZW5kX2lkOmNsaWVudF9zZWNyZXQ=
is a base64
encoding of client_id:client_secret
, of the client with enabled the introspection capability.
Send a Request to the introspection endpoint, when a previous response is not cached:
<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 Response of the introspection
endpoint, using the Access Token
as a key and a duration
of the cache derived from the Cache-control
header of the response.
Cache the response from introspection endpoint:
<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 response from the introspection
endpoint is then parsed (by accessing the cached value) and 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
received form the introspection
endpoint.
Continue or block the request according to the introspection status:
<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
The complete policy can be found in Appendix A: – Require OAuth Token Policy
Introspect with application/jwt as accept header
Curity can also respond to requests in the introspection endpoint with the Accept: application/jwt
header. When introspecting a valid access token, Curity responds with 200 OK
and the JWT in the body of the response. An expired or invalid access token, causes Curity 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, also, Curity’s status codes on the introspection request are taken in consideration and the gateway responds accordingly.
Introspection request using application/jwt as accept header:
<set-header name="Accept" exists-action="override"><value>application/jwt</value></set-header>
Handle introspection response and different status codes:
<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>
Appendix A: – Require OAuth Token Policy
Azure API Management Policy that requires a valid Access Token:
Appendix B: – Application/JWT Policy
Azure API Management Policy which uses Accept: application/jwt header:
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.
Authorize developer accounts
In order to be able to send authorized requests from the Azure Portal, you first need to register the OAuth server as an authorization server with Azure.
-
Go to the API Management service in the Azure Portal
-
Select OAuth 2.0 from the menu on the left
-
Click Add
-
Fill in the required information:
- Name (used to reference this authorization server)
- Client registration page URL (not required, add a placeholder:
https://login.example.com/
) - Make sure Authorization code is selected.
- Authorization endpoint URL (i.e.
https://login.example.com/oauth/v2/oauth-authorize
) - Under Authorization request method, make sure
POST
is selected. - Token endpoint URL (i.e.
https://login.example.com/oauth/v2/oauth-token
) - For Client authentication methods, select
Basic
- Client ID, Client secret (This is the ID and secret of the OAuth app with Code flow capability, not the client ID and client secret for the OAuth app with the introspection capability which is used from the policy described above)
- The redirect_uri provided in this page, must be configured as the redirect_uri of the client used in the previous fields.
- Save your changes
-
Click APIs from the menu on the left
-
Click on your API and switch to the Settings tab
-
Under Security and User authorization select OAuth 2.0 and set as OAuth 2.0 server the one you configured earlier.
-
Click Save
Now you are able to use your deployment as an Authorization server within the Developer portal. Go to the Developer portal -> Portal overview -> Developer portal (legacy) at the top and in the new browser window that opens select APIs from the top menu. Click on your API, choose Try it. Under the Authorization headers, you will find the previously configured server. Try sending requests with No auth option and with Authorization code to understand how the API management behaves.
Note
A very handy public API to test your configuration is httpbin. You can import it through the Publisher Portal.
Example request in /headers endpoint of httpbin, proxied by Azure API Management:
GET https://example.azure-api.net/httpbin/headers HTTP/1.1Host: example.azure-api.netOcp-Apim-Trace: trueOcp-Apim-Subscription-Key: <azure-subscription-key>Authorization: Bearer afbd672t-g215-th81-l8v1-6ef4rc57ly1v
The body of the response will be something like the following, when the Access Token
and azure-subscription-key
are correct.
Body of a successful response:
{"headers": {"Authorization": "Bearer <JWT>","Connection": "close","Host": "httpbin.org","Ocp-Apim-Subscription-Key": "<azure-subscription-key>"}}
An unauthorized request will return the following:
Response status: 401 UnauthorizedDate: Tue, 18 Apr 2017 13:57:14 GMTWWW-Authenticate: Bearer, error="invalid_token"Content-Length: 0
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