Are you using Azure API Management to forward requests on to an external API service that requires OAuth tokens for authorization? In this post we will cover how you can use policy in APIM to automatically generate and cache OAuth 2.0 tokens and include the tokens in headers before forwarding requests to the backend.
The approach we are are going to take to achieve this is:
- Store our client id, client secret and identity provider URL as named values
- Make changes to the API root policy to:
- Check if we have an existing OAuth 2.0 token in cache
- If not, we will fetch the client id and client secret and store them in variables
- Generate a new access token
- Store the access token in cache
- Add the access token to the HTTP request in a header
- Forward the request to the backend
Named values
Navigate to Named values to create three named values to store the client id, client secret, and identity provider URL. Named values can be accessed within API policies and are an effective way to be able to dynamically retrieve configuration variables. See the below screenshot which shows the three named values I created for this demo.
Policy
Now that we have our name values set, we will update the root policy of the API so that every operation we call as part of the API will automatically inherit the policy to handle OAuth. To get to the root policy of the API, select your API, click on All operations, and then click on the code icon next to Policies as highlighted below.
The below is an example policy that can be used to fetch an OAuth 2.0 token from Azure Active Directory. If Azure Active Directory is not the identity provider, then you may need to adjust the policy if the response returned from the provider has a different JSON structure.
<policies>
<inbound>
<base />
<cache-lookup-value key="oauth-token-key" variable-name="oauth-token" caching-type="internal" />
<choose>
<when condition="@(!context.Variables.ContainsKey("oauth-token"))">
<set-variable name="clientid" value="{{example-client-id}}" />
<set-variable name="clientsecret" value="{{example-client-secret}}" />
<set-variable name="tokenurl" value="{{example-identity-token-endpoint}}" />
<set-variable name="scope" value="https://api.adamgatt.com.au" />
<send-request ignore-error="false" timeout="20" response-variable-name="jwt" mode="new">
<set-url>@((string)context.Variables["tokenurl"])</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/x-www-form-urlencoded</value>
</set-header>
<set-header name="Authorization" exists-action="override">
<value>@("Basic " + System.Convert.ToBase64String(Encoding.UTF8.GetBytes((string)context.Variables["clientid"] + ":" + (string)context.Variables["clientsecret"])))</value>
</set-header>
<set-body>@{
return $"grant_type=client_credentials&scope={(String)context.Variables["scope"]}/.default";
}</set-body>
</send-request>
<set-variable name="authenticationResults" value="@(((IResponse)context.Variables["jwt"]).Body.As<JObject>())" />
<set-variable name="isValidToken" value="@((string)((JObject)context.Variables["authenticationResults"])["access_token"])" />
<choose>
<when condition="@(context.Variables.GetValueOrDefault("isValidToken") == null)">
<return-response>
<set-status code="401" reason="@(context.Response.StatusReason)" />
<set-header name="WWW-Authenticate" exists-action="override">
<value>Bearer error="invalid_token"</value>
</set-header>
<set-body>@{
return $"Error: {(string)((JObject)context.Variables["authenticationResults"])["error_description"]}";
}</set-body>
</return-response>
</when>
<otherwise>
<set-variable name="oauth-token" value="@((string)((JObject)context.Variables["authenticationResults"])["access_token"])" />
<cache-store-value key="oauth-token-key" value="@((String)context.Variables["at-token"])" duration="3300" caching-type="internal" />
</otherwise>
</choose>
</when>
</choose>
<set-header name="Authorization" exists-action="override">
<value>@{
return $"Bearer {(String)context.Variables["at-token"]}";
}</value>
</set-header>
<set-backend-service base-url="https://api.adamgatt.com.au" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
The policy above does the following:
- Attempts to set a context variable called oauth-token with the value of an existing OAuth access token that may exist. If it exists, the value is set in the oauth-token variable.
- The policy then checks if the oauth-token variable exists. If it doesn’t, it means that there wasn’t an existing access token in the cache that could be used, so we need to generate another one.
- If we need to generate another token, the policy then:
- Sets 4 variables, the first 3 of which are our named values, and the fourth being set directly in the policy
- Declares that we want to send a HTTP request, and we want the response of it to be stored in a variable named jwt.
- Set the url to make the HTTP request to one of our variables
- Set the method of the HTTP request as POST
- Set the Content-Type header to include in the HTTP request
- Set the Authorization header as Basic auth with the encoded client id and client secret variables to include in the HTTP request
- Set the body of the HTTP request to include the grant_type and scope
- Send the request
- Create a variable named authenticationResults and set the value to the response of the HTTP request
- Create a variable called isValidToken and attempt to set the value of it with the access_token returned in the HTTP request
- Check if the isValidToken variable is null
- If the isValidToken variable is null, then we declare that we want to return an error response:
- Set the status code to return with the error response as 401
- Set the StatusReason to return with the error response with the StatusReason returned by the HTTP Request
- Set the WWW-Authenticate header to return with the error response as Bearer error=”invalid_token”
- Set the body of the error response to the error_description that the HTTP request returned.
- Return the error response
- If the isValidToken is not null, then:
- Set the oauth-token context variable with the value of the OAuth access token
- Store the OAuth access token in cache for 3300 seconds – we set this duration as Azure Active Directory tokens have a 3600 seconds expiry by default
- Now that we have an OAuth access token in the oauth-token variable (either from cache or just generated), we set the Authorization header as Bearer with the OAuth access token value
- We set the backend service base url that the API operation we forward the request to will send the API request to.