In my previous post, we discussed using Azure API Management (APIM) as the API gateway to expose the Cloud Flows with HTTP Request Trigger endpoints. One of the main questions that came up was around security, specifically around the topic of tightening access to backend APIs. In this post, we are going to delve into this particular topic and see how we can utilise Azure API management policies to further secure and restrict access to our backend APIs.
I’m going to approach this topic in multiple posts, in this first post we are going to cover the following:
- What are Azure API Management policies?
- Protect API operations with Azure AD and the
validate-jwt
policy - Configure managed identity to authenticate with Azure Function
What are Azure API Management Policies?
In the official documentation, the APIM policies are defined as follows:
In Azure API Management (APIM), policies are a powerful capability of the system that allow the publisher to change the behavior of the API through configuration. Policies are a collection of Statements that are executed sequentially on the request or response of an API.
Policies in Azure API Management – docs.microsoft.com
To put it simply, policies are a bunch of rules and conditions that are applied by the APIM to the inbound requests and outbound responses of the backend APIs it manages. Many out-of-the-box policies can be configured and are generally grouped into categories such as access restriction, authentication, caching, cross-domain, transformation, etc. (a full list of the policy reference can be found here).
Protecting API Operations With Azure AD and the validate-jwt
Policy
Most often, the backend APIs are secured by Azure AD and we need to use OAuth 2.0 authentication and authorization to access the resource. To expose these APIs via APIM requires strategies around access restrictions and authentication on the inbound requests before it calls the backend APIs. That’s where policies come into play. To demonstrate this scenario, let’s set up the following:
- A simple Azure Function to act as our backend API secured by Azure AD
- Set up an Azure app registration for the client app that calls the backend API
- Validate and acquire an access token for the client app using Postman
- Set up APIM with the backend API and policies
#1 A simple Azure Function to act as our backend API secured by Azure AD
First, set up an Azure Function project that will act as our backend API and publish it to Azure. To keep things simple, let’s create a quickstart C# Azure Function project using these instructions. After completing the quickstart steps, we should have an HTTP endpoint that can be called as follows:
https://<unique-name>.azurewebsites.net/api/HttpExample?code=<code>
Next, we need to secure the above Azure Function with Azure AD:
- In Azure Portal, navigate to the Azure Function app we just created and select Authentication from the left navigation
- On the Authentication page, click the Add identity provider button and select Microsoft from the identity providers dropdown
- On the Add an identity provider blade, populate the following and then click Add:
#2 Set up an Azure app registration for the client app that consumes the backend API
With the API secure and app registration created for the API, let’s create app registration for the client app that will be consuming this API:
- Navigate to the App Registration section of the Azure Portal (Use this link to navigate directly) and select + New Registration
- On the Register an Application page, enter the following information:
- Name:
client-apim-demo
- Supported account types:
Accounts in this organizational directory only
- Redirect URI:
leave it blank for now
- Name:
- On the Overview page, copy and note down the
Application (client) ID
- On the API permissions page, click + Add a permission and on My APIs tab, select the app registration created for the Azure Function API.
- On the Request API permissions blade, select the following and click Add permissions:
Take a note of the Application ID URI (i.e. api://<guid>) as we will use this later when we configure the APIM policy
- On the Certificates & secrets page, click + New client secret and create a secret then note down the
Value
once created
#3 Validate and acquire an access token for the client app using Postman
We are going to use Postman to acquire the access token for the client app. By examining the claims returned in the token, it will aid our understanding when configuring validate-jwt
policy in step #4.
To acquire the access token, we are going to use client credentials grant flow with client id and the secret to authenticate against Azure AD. We will use values we noted down in step #2 and I have it configured to retrieve these values from the Postman Environment variables. Take special note of the required scope value api://<guid>/.default
, this refers to the application ID URI defined in our Azure Function API app registration. Since we are authenticating as an application and not as a user, we must use the /.default
scope for requesting any application permissions registered against the client app registration (currently we have none).
Sending this request will return an access_token
in the response payload. Take a copy and examine it using jwt.ms. There are many goodies in there but what we are looking for is the audience and issuer claims in the token:
{
"typ": "JWT",
...
}.{
...
"aud": "api://a7e7ae7a-a421-4058-ab5f-6624b5303361",
"iss": "https://sts.windows.net/<tenant-id>/",
"exp": 1626756692,
...
}.[Signature]
#4 Set up APIM with the backend API and policies
Now the fun part! Let’s set up our API in APIM 😎. Please follow these instructions to create an APIM resource instance if you haven’t got one already.
To add a new API from Function App, go to your APIM instance in Azure Portal, navigate to APIs > APIs on the left navigation, click + Add API and select Function App from the array of Create from Azure resource selections. Populate the details as follows and click Create:
When selecting the Function app in the dialog above, I selected the Azure Function we created in #1 and selected the HttpExample endpoint:
With the GET HttpExample
operation automatically created for us, click Policies </> on the Inbound processing to configure our policies:
Add the following inbound policies:
<policies> <inbound> <base /> <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid." require-expiration-time="true" require-scheme="Bearer" require-signed-tokens="true"> <openid-config url="https://login.microsoftonline.com/{{tenant-id}}/v2.0/.well-known/openid-configuration" /> <audiences> <audience>api://a7e7ae7a-a421-4058-ab5f-6624b5303361</audience> </audiences> <issuers> <issuer>https://sts.windows.net/{{tenant-id}}/</issuer> </issuers> </validate-jwt> <set-backend-service id="apim-generated-policy" backend-id="afa-d-sandboxfunctionssecureapi" /> <set-header name="Ocp-Apim-Subscriptionkey" exists-action="delete" /> </inbound> <backend> <base /> </backend> <outbound> <base /> </outbound> <on-error> <base /> </on-error> </policies>
Note: Named values are a global collection of name/value pairs in APIM instance. We can refer to these name/value pairs in expressions like {{name}} in the policies, e.g. {{tenant-id}}.
The role of the validate-jwt
policy is to pre-authorise the request by examining the validity of the JSON Web Token (JWT) present in the request. If the token is either absent or invalid, it will prevent the inbound request from executing, and instead send back a 4xx HTTP status code and an error message in the response detailing the issue.
In our validate-jwt
policy we are specifically checking for:
Entry | Comment |
---|---|
header-name=”Authorization” | Specify that the token is present in the Authorization header |
failed-validation-httpcode=”401″ | Return 401 if the token is invalid |
failed-validation-error-message=”Unauthorized. Access token is missing or invalid.” | Return custom error message if the token is invalid |
require-expiration-time=”true” | Checks for “exp” (expiration time) in the token claim |
require-scheme=”Bearer” | Checks for “Bearer” token scheme in the Authorization header |
require-signed-tokens=”true” | Checks if the token is signed |
openid-config url=”https://login.microsoftonline.com/{{tenant-id}}/v2.0/.well-known/openid-configuration” | OpenID Connect metadata document endpoint to validate the signed key and the issuer claims in the token. This endpoint URL can be obtained from the App Registration page in Azure Portal under Overview > Endpoints |
<audience>api://a7e7ae7a-a421-4058-ab5f-6624b5303361</audience> | Checks if the “aud” claim in the token matches the audience specified in the policy |
<issuer>https://sts.windows.net/{{tenant-id}}/</issuer> | Checks if the “iss” claim in the token matches the issuer specified in the policy |
Since policies are executed sequentially, if the check for validate-jwt
policy fails, it won’t continue to the next specified policy.
To test this, navigate to the HttpExample API operation in APIM and access the Test tab. Add an Authorization header with the bearer token generated from the Postman and click Send:
This should return 200 HTTP status code and a response from the Azure Function API.
Configure Managed Identity to Authenticate with Azure Function
It is always advisable to pre-authorize the request with the validate-jwt
policy when the backend API is secured by an identity provider. In our scenario, once the token is successfully validated, it passes the same authorization header to the backend API unless explicitly removed using the set-header
policy with exists-action="delete"
. Therefore, for these calls to succeed, the caller of the API (client) must always generate a token and send it with the request.
In the case when the backend API is hosted on an Azure Resource that supports Azure AD authentication, we can utilise managed identities to eliminate the need to manage credentials such as client id/client secrets to generate the access token in the first place. Enabling the managed identity on APIM will use the managed identity to acquire the Azure AD token to pass to the Azure Function. Please note that there is a certain amount of trust here so only use it on APIs that are exposed to trusted clients.
To enable the managed identity on APIM:
- Navigate to your API Management instance in Azure Portal and select the Security > Managed identities from the left navigation
- On the System assigned tab, set the Status =
On
Update the inbound policies:
<policies> <inbound> <base /> <set-backend-service id="apim-generated-policy" backend-id="afa-d-sandboxfunctionssecureapi" /> <set-header name="Ocp-Apim-Subscriptionkey" exists-action="delete" /> <set-header name="Authorization" exists-action="delete" /> <authentication-managed-identity resource="a7e7ae7a-a421-4058-ab5f-6624b5303361" /> </inbound> <backend> <base /> </backend> <outbound> <base /> </outbound> <on-error> <base /> </on-error> </policies>
The authentication-managed-identity
policy has the resource
parameter that takes the client id of the API app registration. When you test this, you no longer need to pass the Authorization
header with the bearer token.
Ok, that’s it for this first post! Please look out for my second post if you would like to learn more about this topic.
As always, hope you enjoyed the post, let me know your thoughts in the comments below or any of my contact channels 😎