Microsoft Azure API Management

The following section describes the steps needed in order to create a Azure API Management instance that will act as a proxy between a Mobile or Web app and an API.

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.

../../_images/introspect-procedure.png
Listing 305 Example of an introspection procedure that returns a JWT
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function result(context) {
  'use strict';
  var responseData = {
      active: context.presentedToken.active
  };

  if (context.presentedToken.active) {
      var accessTokenData = context.presentedToken.data;
      var issuedDelegation = context.delegation;

      // issue as JWT
      var issuedAccessToken = context.getAccessTokenIssuer("jwtAccessTokenIssuer").issue(accessTokenData, issuedDelegation);

      appendObjectTo(context.presentedToken.data, responseData);
      responseData.token_type = context.presentedToken.type;
      responseData.client_id = context.presentedToken.delegation.clientId;
      responseData.expired_scope = context.presentedToken.expiredScopes;
      responseData.jwt = issuedAccessToken;
  }

  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.

../../_images/introspection-app-capabilities.png

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.

../../_images/oauth-introspection-endpoint.png

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.

../../_images/api-management-create-instance-menu.png

The next step is to import or create an API. As soon as the instance has started, you can configure your APIs in the Publisher Portal. Click APIs from the menu and Add or Import your API. The Echo API is created when you create the API Management instance.

../../_images/publisher-developer-portal-links.png

You can add a Policy to your API by clicking Policies from the menu. You can apply a Policy to an arbitrary operation of your API, or to the entire API when you don’t select any operation.

../../_images/global-api-policy.png

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 by Curity.

API Policy

This section is based on 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.

Listing 306 Extract token from Authorization Header
1
<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.

Listing 307 Lookup cache for AT-JWT association
1
<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 always are for active` tokens, so they should contain the ``jwt in their body.

Listing 308 Forward the request while replacing AT with JWT
1
2
3
<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 5 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.

Listing 309 Send a Request to the introspection endpoint, when a previous response is not cached
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<send-request mode="new" response-variable-name="tokenstate" timeout="20" ignore-error="true">
      <set-url>https://login.example.com/oauth/v2/introspection</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.

Listing 310 Cache the response from introspection endpoint
1
2
3
4
5
6
7
<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.

Listing 311 Continue or block the request according to the introspection status
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<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.

Listing 312 Introspection request using application/jwt as accept header
1
2
3
<set-header name="Accept" exists-action="override">
  <value>application/jwt</value>
</set-header>
Listing 313 Handle introspection response and different status codes
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<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))">
    <!-- active is equal to false, return 401 Unauthorized -->
    <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 responsds 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

Listing 314 Azure API Management Policy that requires a valid Access Token
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<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 dont 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/introspection</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

Listing 315 Azure API Management Policy which uses Accept: application/jwt header.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<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 dont 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/introspection</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))">
        <!-- active is equal to false, return 401 Unauthorized -->
        <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 responsds 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 Developer Portal. The Developer 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 Developer Portal, you first need to register the OAuth server as an authorization server with Azure.

  1. Go to the Publisher Portal
  2. Select Security from the menu on the left
  3. Switch to the Auth 2.0 tab and press “Add Authorization Server”
  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/ )
  • Authorization endpoint URL (i.e. https://login.example.com/oauth/v2/authorize)
  • Token endpoint URL (i.e. https://login.example.com/oauth/v2/token)
  • 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 that page, must be configured as the redirect_uri of the client used in the previous fields.
  • Save your changes
  1. Click APIs from the menu on the left
  2. Click on your API and switch to the Security tab
  3. Under User authorization select OAuth 2.0 and set as Authorization server the one you configured earlier.

Now you are able to use your deployment as an Authorization server within the Developer Portal. Go to the Developer Portal and select APIs from the top menu. Click on your API, select an operation from the left and press 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.

Listing 316 Example request in /headers endpoint of httpbin, proxied by Azure API Management
1
2
3
4
5
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.

Listing 317 Body of a successful response
1
2
3
4
5
6
7
8
{
  "headers": {
    "Authorization": "Bearer <JWT>",
    "Connection": "close",
    "Host": "httpbin.org",
    "Ocp-Apim-Subscription-Key": "<azure-subscription-key>"
  }
}

An unauthorized request will return the following:

Listing 318 Unauthorized request
1
2
3
4
Response status: 401 Unauthorized
Date: Tue, 18 Apr 2017 13:57:14 GMT
WWW-Authenticate: Bearer error="invalid_token"
Content-Length: 0