Microsoft Azure API Management

Microsoft Azure API Management

tutorials

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 from Curity.
  2. Curity responds with the Access Token.
  3. The application requests access to the remote resource (i.e. 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 is already found active previously.
  5. In case the Access Token was not verified earlier, it sends a request to the introspection endpoint.
  6. Curity replies to this request according to RFC 7662. The body of the response contains a JSON with a boolean active property.
    1. When the Access Token is not active, the API Proxy replies to the application with status code 401 Unauthorized
    2. 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).
  7. 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.
  8. 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.

Introspect Procedure

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.

Introspection App Capabilities

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.

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

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.

Add new API

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.

Add policy

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:

<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>

Appendix B: – Application/JWT Policy

Azure API Management Policy which uses Accept: application/jwt header:

<policies>
  <inbound>
    <set-variable name="token" value="@(context.Request.Headers.GetValueOrDefault("Authorization","scheme param").Split(' ').Last())" />
    <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>
    <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-header name="Accept" exists-action="override">
            <value>application/jwt</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>
    <cache-lookup-value key="@((string)context.Variables["token"])" variable-name="introspectToken" />
    <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>
    <base />
  </inbound>
  <backend>
    <base />
  </backend>
  <outbound>
    <base />
  </outbound>
  <on-error>
    <base />
  </on-error>
</policies>

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.

  1. Go to the API Management service in the Azure Portal

  2. Select OAuth 2.0 from the menu on the left

  3. Click Add

  4. 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
  5. Click APIs from the menu on the left

  6. Click on your API and switch to the Settings tab

  7. Under Security and User authorization select OAuth 2.0 and set as OAuth 2.0 server the one you configured earlier.

  8. 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.1
Host: example.azure-api.net
Ocp-Apim-Trace: true
Ocp-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 Unauthorized
Date: Tue, 18 Apr 2017 13:57:14 GMT
WWW-Authenticate: Bearer, error="invalid_token"
Content-Length: 0

Let’s Stay in Touch!

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

Keep up with our latest articles and how-tos using RSS feeds