Creating Validate JWT Policy in Azure APIM

Zaki Mohammed Zaki Mohammed
Dec 27, 2023 | 3 min read | 244 Views | Comments

Validating JWT is nothing new in the context of API development. Thousands of tokens been validated right now while you are reading this text. Azure APIM makes your life easy for API development by allowing you to create a validate-jwt policy to perform such a necessary task. In this article, we will understand how to create such policy under Azure APIM.

Azure APIM offers tons of features related to API Management one of which is its policies (inbound, outbound). These policies are handy to use and easy to configure and can be applied to individual endpoint, individual API, all APIs depending on your requirements. One of such inbound policy is "validate-jwt" (read more from Validate Jwt).

The validate-jwt policy provide you a way to validate the received JWT token at APIM level itself, before even reaching to your backend, this will help you to eliminate the redundant code logic you required on different APIs. You can manage the validation logic at one place. It provides many options to configure the validation criteria such as audience, issuer, openid config, etc.

In this article, we will go through the code snippets and supporting steps to understand how to create and apply validate-jwt policy. We will head in this direction:

  • Create Validate JWT Policy
  • Properties and Tags
  • Consuming Output Variable
  • Creating Policy Fragment

Create Validate JWT Policy

Follow below steps to add policy to your API endpoint:

  • Select the API endpoint or All Operations or All APIs.
  • Click on the "Policy Editor" button present in the "Inbound processing" section.
  • Add the policy code below the "<base />" tag within "<inbound>" section.

Below shows the template of the "validate-jwt" policy:

<validate-jwt 
	header-name="Authorization" 
	failed-validation-httpcode="401" 
	failed-validation-error-message="Unauthorized" 
	require-scheme="Bearer" 
	output-token-variable-name="output-token">

	<openid-config url="https://login.microsoftonline.com/#{YOUR_TENANT_ID_1}#/v2.0/.well-known/openid-configuration/" />
	<openid-config url="https://login.microsoftonline.com/#{YOUR_TENANT_ID_2}#/v2.0/.well-known/openid-configuration/" />
	
	<audiences>
		<audience>#{YOUR_AUDIENCE_1}#</audience>
		<audience>#{YOUR_AUDIENCE_2}#</audience>
	</audiences>

	<issuers>
		<issuer>https://login.microsoftonline.com/#{YOUR_TENANT_ID_1}#/v2.0</issuer>
		<issuer>https://login.microsoftonline.com/#{YOUR_TENANT_ID_2}#/v2.0</issuer>
	</issuers>
</validate-jwt>

Here, we have a template of "validate-jwt" policy with some placeholder values like YOUR_TENANT_ID, YOUR_AUDIENCE. Notice that the format is the one which is compatible with build pipelines present with most of the CI/CD tools (#{TOKEN}#).

Properties and Tags

Let us break down the policy and understand the what-is-what, for this below shows the validate-jwt policy with some fake real values:

<validate-jwt 
	header-name="Authorization" 
	failed-validation-httpcode="401" 
	failed-validation-error-message="Unauthorized" 
	require-scheme="Bearer" 
	output-token-variable-name="output-token">

	<openid-config url="https://login.microsoftonline.com/contoso.onmicrosoft.com/v2.0/.well-known/openid-configuration/" />
	
	<audiences>
		<audience>f0047ad9-83fb-4a82-8167-d253b0dfb0c3</audience>
		<audience>08c06aea-ea55-48d4-9611-a43c53bf0955</audience>
	</audiences>

	<issuers>
		<issuer>https://login.microsoftonline.com/31537af4-6d77-4bb9-a681-d2394888ea26/v2.0</issuer>
	</issuers>
</validate-jwt>

Properties

  • header-name: The header which contains the supplied token.
  • failed-validation-httpcode: Failed respones code.
  • failed-validation-error-message: The failed response message text (will appear within a JSON object).
  • require-scheme: The scheme using which the token is validated.
  • output-token-variable-name:
    • The token variable name provided by the policy once the token is validated successfully.
    • A custom variable name can be given, make sure to use the exact same variable accross.
    • The token object belongs to the Jwt class (read more from the doc).
    • In the policy it named as output-token.

Tags

  • openid-config:
    • The open id configuration based on which the token will be validated.
    • In the policy we have contoso open id configuration URLs.
    • It is required to have appropriate openid-config url based on the issuers.
    • Can have multiple openid-config within one policy.
  • audiences:
    • The audiences based on which the token is validated against.
    • Can have multiple audiences.
    • In the policy we have 2 fake uuid generated audiences.
  • issuers:
    • The issuers based on which the token is validated against.
    • Can have multiple issuers.
    • In the policy we have contoso issuer configured.

Consuming Output Variable

The next question would be "how to consume the output variable" or "how to receive this variable at the backend once verified". You can simply set a custom header in the policy flow just after the validate-jwt policy using "set-header" tag.

<validate-jwt output-token-variable-name="output-token">...</validate-jwt>

<set-header name="CO-Jwt-Token" exists-action="override">
	<value>@(JsonConvert.SerializeObject(((Jwt)context.Variables["output-token"])))</value>
</set-header>

Here, we have added the "set-header" tag with name "CO-Jwt-Token" (you can give any name, we have given a prefix CO - CodeOmelet followed by the name of the header). Make sure to set the property name separated with hyphens and not with underscore or spaces. The value contains the entire output object of class Jwt. The value is obtained through the context variable. Make sure to serialize the object. If you are curious about the @() syntax, they are called as Policy Expression, read more from APIM Policy Expression. Also, if you want to know what is there inside the Jwt class you can checkout from this table Context Variables. As per the docs, the Jwt class contains below properties:

public class Jwt
{
	public string Algorithm { get; set; }
	public string Type { get; set; }
	public string Issuer { get; set; }
	public string Subject { get; set; }
	public IEnumerable<string> Audiences { get; set; }
	public DateTime? ExpirationTime { get; set; }
	public DateTime? NotBefore { get; set; }
	public DateTime? IssuedAt { get; set; }
	public object Id { get; set; }
	public IReadOnlyDictionary<string, string[]> Claims { get; set; }
}

Below shows the values obtained under this class:

{
    "Algorithm": "RS256",
    "Type": "JWT",
    "Issuer": "https://login.microsoftonline.com/31537af4-6d77-4bb9-a681-d2394888ea26/v2.0",
    "Subject": "5794bd0d-d246-4990-96ea-b71d9d4f48f7",
    "Audiences": ["f0047ad9-83fb-4a82-8167-d253b0dfb0c3"],
    "ExpirationTime": "2022-11-17T12:10:59Z",
    "NotBefore": "2022-11-17T10:40:53Z",
    "IssuedAt": "2022-11-17T10:40:53Z",
    "Id": null,
    "Claims": {
        "aud": ["f0047ad9-83fb-4a82-8167-d253b0dfb0c3"],
        "iss": ["https://login.microsoftonline.com/31537af4-6d77-4bb9-a681-d2394888ea26/v2.0"],
        "iat": ["1800217653"],
        "nbf": ["1800217653"],
        "exp": ["1800223059"],
        "aio": ["dc710844-0236-43dc-994a-bbb6cc621650"],
        "azp": ["f0047ad9-83fb-4a82-8167-d253b0dfb0c3"],
        "azpacr": ["0"],
        "name": [""],
        "username": ["John Doe"],
        "email": ["johndoe@gmail.com"],
        "rh": ["b72e16db-f31b-433b-aff8-7b10f3d50926"],
        "scp": ["user"],
        "sub": ["462f597e-c545-4f42-98e2-6ff35cfea095"],
        "tid": ["63593dd6-ac93-4978-8dae-aae06850ddad"],
        "uti": ["aae06850ddad"],
        "ver": ["2.0"]
    }
}

Conditionally Applying Header

<validate-jwt output-token-variable-name="output-token">...</validate-jwt>

<choose>
	<when condition='@((Jwt)context.Variables["output-token"] != null)'>
		<set-header name="CO-Jwt-Token" exists-action="override">
			<value>@(JsonConvert.SerializeObject(((Jwt)context.Variables["output-token"])))</value>
		</set-header>
	</when>
</choose>

Here, we are optionally attaching the header only if the variable obtains the validate-jwt output using the "choose-when" tags.

Obtaining Specific Claims

<validate-jwt output-token-variable-name="output-token">...</validate-jwt>

<choose>
	<when condition='@(!string.IsNullOrEmpty((string)((Jwt)context.Variables["output-token"]).Claims.GetValueOrDefault("username")))'>
		<set-header name="CO-Username" exists-action="override">
			<value>@((string)((Jwt)context.Variables["output-token"]).Claims.GetValueOrDefault("username"))</value>
		</set-header>
	</when>
</choose>
<choose>
	<when condition='@(!string.IsNullOrEmpty((string)((Jwt)context.Variables["output-token"]).Claims.GetValueOrDefault("email")))'>
		<set-header name="CO-Email" exists-action="override">
			<value>@((string)((Jwt)context.Variables["output-token"]).Claims.GetValueOrDefault("email"))</value>
		</set-header>
	</when>
</choose>

Here, we are getting the claims like "username" and "email" and attaching it as a custom header if they existed using choose-when tags.

Creating Policy Fragment

 If we want to use the same policy across the APIM, we can simply create Policy Fragment. We can have them defined once and used at many places. Below we have created a policy named "ValidateJwtFragment".

<fragment>
	<!-- policy -->
	<validate-jwt 
		header-name="Authorization" 
		failed-validation-httpcode="401" 
		failed-validation-error-message="Unauthorized" 
		require-scheme="Bearer" 
		output-token-variable-name="output-token">

		<openid-config url="https://login.microsoftonline.com/contoso.onmicrosoft.com/v2.0/.well-known/openid-configuration/" />
		
		<audiences>
			<audience>f0047ad9-83fb-4a82-8167-d253b0dfb0c3</audience>
			<audience>08c06aea-ea55-48d4-9611-a43c53bf0955</audience>
		</audiences>

		<issuers>
			<issuer>https://login.microsoftonline.com/31537af4-6d77-4bb9-a681-d2394888ea26/v2.0</issuer>
		</issuers>
	</validate-jwt>
	
	<!-- custom-headers -->
	<choose>
		<when condition='@(!string.IsNullOrEmpty((string)((Jwt)context.Variables["output-token"]).Claims.GetValueOrDefault("username")))'>
			<set-header name="CO-Username" exists-action="override">
				<value>@((string)((Jwt)context.Variables["output-token"]).Claims.GetValueOrDefault("username"))</value>
			</set-header>
		</when>
	</choose>
	<choose>
		<when condition='@(!string.IsNullOrEmpty((string)((Jwt)context.Variables["output-token"]).Claims.GetValueOrDefault("email")))'>
			<set-header name="CO-Email" exists-action="override">
				<value>@((string)((Jwt)context.Variables["output-token"]).Claims.GetValueOrDefault("email"))</value>
			</set-header>
		</when>
	</choose>
</fragment>

We can now simply add this to API endpoint or All Operations or All APIs like shown below:

<policies>
	<inbound>
		<base />
		<include-fragment fragment-id="ValidateJwtFragment" />
	</inbound>
	<backend>
		<base />
	</backend>
	<outbound>
		<base />
	</outbound>
	<on-error>
		<base />
	</on-error>
</policies>

You can find the above codes from the Gist created and attached to this article.

That's all folks.


Zaki Mohammed
Zaki Mohammed
Learner, developer, coder and an exceptional omelet lover. Knows how to flip arrays or omelet or arrays of omelet.