Azure API Management JWT Validation Policy
Securing APIs with Token-Based Authentication
Introduction
JWT (JSON Web Token) validation is essential for securing APIs in Azure API Management. The validate-jwt policy ensures that only valid, properly signed tokens can access your backend services. This protects your APIs from unauthorized access and enables integration with identity providers like Azure Active Directory (Entra ID).
This comprehensive guide covers:
- JWT fundamentals — Understanding JWT structure
- Policy configuration — All validation options
- Identity provider integration — Azure AD, Auth0, others
- Claim-based authorization — Role and permission checks
- Advanced patterns — Multiple audiences, custom validation
JWT Structure
Three Parts of a JWT
┌───────────────────────────────────────────────────────────────────────────────┐
│ JWT STRUCTURE │
├───────────────────────────────────────────────────────────────────────────────┤
│ │
│ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiIxMjM0NTY3ODkw . │
│ ═══════════════════════════════ ════════════════════ │
│ Header Payload │
│ { │
│ { "sub": "1234567890", │
│ "alg": "HS256", "name": "John Doe", │
│ "typ": "JWT" "iat": 1516239022, │
│ } "roles": ["user"] │
│ } │
│ │
│ ═══════════════════════════════════════════════════════════ │
│ Signature (HMAC SHA256) │
│ HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), │
│ secret) │
│ │
└───────────────────────────────────────────────────────────────────────────────┘
JWT Claims
| Claim | Description |
|---|---|
iss | Issuer - who created the token |
sub | Subject - who the token is about |
aud | Audience - intended recipient |
exp | Expiration time |
iat | Issued at time |
roles | Roles assigned to user |
scp | OAuth scopes |
Basic JWT Validation
Simple Validation
<inbound>
<validate-jwt header-name="Authorization" />
</inbound>
This minimal configuration:
- Looks for token in Authorization header
- Expects Bearer token format
- Validates signature using configured keys
- Checks token expiration
Full Validation Configuration
<inbound>
<validate-jwt header-name="Authorization">
<openid-config url="https://login.microsoftonline.com/tenant-id/v2.0/.well-known/openid-configuration" />
<audiences>
<audience>api://my-api-client-id</audience>
</audiences>
<issuers>
<issuer>https://login.microsoftonline.com/tenant-id/v2.0</issuer>
</issuers>
<issuer-signing-keys>
<key>base64-encoded-key</key>
</issuer-signing-keys>
</validate-jwt>
</inbound>
Configuration Options
Header Name
<!-- Default: Authorization header -->
<validate-jwt header-name="Authorization" />
<!-- Custom header -->
<validate-jwt header-name="X-Auth-Token" />
<!-- Query parameter -->
<validate-jwt query-parameter-name="token" />
OpenID Configuration
<validate-jwt header-name="Authorization">
<!-- Azure AD v2.0 -->
<openid-config url="https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration" />
<!-- Azure AD v1.0 -->
<openid-config url="https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration" />
<!-- Auth0 -->
<openid-config url="https://{your-domain}/.well-known/openid-configuration" />
</validate-jwt>
Audiences
<validate-jwt header-name="Authorization">
<audiences>
<audience>api://my-api-client-id</audience>
<audience>https://myapi.azure-api.net</audience>
</audiences>
</validate-jwt>
Issuers
<validate-jwt header-name="Authorization">
<issuers>
<issuer>https://login.microsoftonline.com/{tenant}/v2.0</issuer>
<issuer>https://sts.windows.net/{tenant}/</issuer>
</issuers>
</validate-jwt>
Signing Keys
<validate-jwt header-name="Authorization">
<!-- Static key (HMAC) -->
<issuer-signing-keys>
<key>base64-encoded-secret-key</key>
</issuer-signing-keys>
<!-- RSA key -->
<issuer-signing-keys>
<key>-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----</key>
</issuer-signing-keys>
</validate-jwt>
Required Claims
Check for Specific Claims
<inbound>
<validate-jwt header-name="Authorization">
<required-claims>
<claim name="roles">
<value>User</value>
<value>Admin</value>
</claim>
<claim name="groups">
<value>Group1</value>
</claim>
</required-claims>
</validate-jwt>
</inbound>
Multiple Values for Single Claim
<required-claims>
<claim name="roles">
<value>User</value>
<value>Reader</value>
</claim>
<claim name="department">
<value>Sales</value>
<value>Marketing</value>
</claim>
</required-claims>
Azure AD (Entra ID) Integration
Step 1: Register Application
- Go to Azure Portal → Microsoft Entra ID
- Register a new application (App registrations)
- Note the Application (client) ID
- Note the Directory (tenant) ID
Step 2: Expose API
- In your app registration, go to "Expose an API"
- Set Application ID URI
- Add scopes (e.g., "access_as_user")
Step 3: Configure Policy
<inbound>
<validate-jwt header-name="Authorization">
<openid-config url="https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid-configuration" />
<audiences>
<audience>api://{client-id}</audience>
</audiences>
<issuers>
<issuer>https://login.microsoftonline.com/{tenant-id}/v2.0</issuer>
</issuers>
</validate-jwt>
</inbound>
Get Tokens from Client App
// Microsoft Authentication Library (MSAL)
var scopes = new[] { "api://{client-id}/access_as_user" };
var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
var token = result.AccessToken;
// Send: Authorization: Bearer {token}
Extract Claims for Backend
Pass Claims to Backend
<inbound>
<validate-jwt header-name="Authorization" output-token-variable-name="jwt-token">
<openid-config url="https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration" />
</validate-jwt>
<set-header name="X-User-Id" exists-action="override">
<value>@(((System.IdentityModel.Tokens.Jwt.JwtSecurityToken)context.Variables["jwt-token"]).Subject)</value>
</set-header>
<set-header name="X-User-Roles" exists-action="override">
<value>@(((System.IdentityModel.Tokens.Jwt.JwtSecurityToken)context.Variables["jwt-token"]).Claims
.Where(c => c.Type == "roles")
.Select(c => c.Value)
.ToList())</value>
</set-header>
</inbound>
Extract Specific Claims
<inbound>
<validate-jwt header-name="Authorization" output-token-variable-name="jwt" />
<set-variable name="userId" value="@{
var token = (System.IdentityModel.Tokens.Jwt.JwtSecurityToken)context.Variables[\"jwt\"];
return token?.Claims?.FirstOrDefault(c => c.Type == \"sub\")?.Value;
}" />
<set-header name="X-User-Id" exists-action="override">
<value>@(context.Variables["userId"])</value>
</set-header>
</inbound>
Role-Based Access Control
Check User Roles
<inbound>
<validate-jwt header-name="Authorization" />
<choose>
<when condition="@(context.Request.Headers.GetValueOrDefault("Authorization", "").Contains("Admin"))">
<!-- Already validated, but check role claim -->
</when>
</choose>
<!-- Use require-admission-control for more control -->
<validate-jwt header-name="Authorization">
<required-claims>
<claim name="roles">
<value>User</value>
</claim>
</required-claims>
</validate-jwt>
</inbound>
Different Policies Per Operation
<!-- Product API - Read -->
<operation name="GetProducts">
<inbound>
<validate-jwt header-name="Authorization">
<required-claims>
<claim name="roles">
<value>Reader</value>
<value>User</value>
<value>Admin</value>
</claim>
</required-claims>
</validate-jwt>
</inbound>
</operation>
<!-- Product API - Write -->
<operation name="CreateProduct">
<inbound>
<validate-jwt header-name="Authorization">
<required-claims>
<claim name="roles">
<value>Admin</value>
</claim>
</required-claims>
</validate-jwt>
</inbound>
</operation>
Advanced Configuration
Multiple Audiences
<validate-jwt header-name="Authorization">
<audiences>
<audience>api://primary-api-client-id</audience>
<audience>https://api.contoso.com</audience>
<audience>https://api.fabrikam.com</audience>
</audiences>
</validate-jwt>
Token Cache
<validate-jwt header-name="Authorization" output-token-variable-name="jwt">
<cache-lookup>
<key>JwtToken:@(context.Request.Headers.GetValueOrDefault("Authorization", "").Substring(0, 50))</key>
<duration>3600</duration>
</cache-lookup>
</validate-jwt>
Custom Error Response
<inbound>
<validate-jwt header-name="Authorization">
<openid-config url="https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration" />
</validate-jwt>
<base />
</inbound>
<on-error>
<choose>
<when condition="@(context.LastError.Message.Contains("IDX"))">
<return-response>
<set-status code="401" reason="Unauthorized" />
<set-header name="WWW-Authenticate" exists-action="override">
<value>Bearer error="invalid_token"</value>
</set-header>
<set-body>{"error": "Invalid or expired token"}</set-body>
</return-response>
</when>
</choose>
</on-error>
Best Practices
| Practice | Description |
|---|---|
| Use OpenID Config | Let APIM fetch signing keys automatically |
| Validate audience | Ensure token is for your API |
| Check expiration | Built-in, but verify TTL is reasonable |
| Require necessary claims | Add role/permission checks |
| Extract claims | Pass user info to backend |
| Handle errors gracefully | Return proper 401 responses |
Security Checklist
✓ Use HTTPS for all traffic
✓ Validate signature using signing keys
✓ Check token expiration
✓ Validate audience claim
✓ Validate issuer claim
✓ Require minimum required claims
✓ Extract claims for backend logging
✓ Return proper error messages
Troubleshooting
Common Errors
| Error | Cause | Solution |
|---|---|---|
IDX10500 | Signature validation failed | Check signing key |
IDX10501 | Audience validation failed | Add correct audience |
IDX10502 | Issuer validation failed | Add correct issuer |
IDX10503 | Expiration time validation failed | Token expired |
IDX10504 | Required claim missing | Add required claim |
Debug JWT
<inbound>
<!-- Log JWT claims for debugging -->
<set-variable name="jwtInfo" value="@{
var auth = context.Request.Headers.GetValueOrDefault(\"Authorization\", \"\");
if (auth.StartsWith(\"Bearer \"))
{
var token = auth.Substring(7);
var parts = token.Split('.');
var payload = System.Text.Encoding.UTF8.GetString(
System.Convert.FromBase64String(parts[1] + \"==\"));
return payload;
}
return \"No token\";
}" />
<log level="information">@(context.Variables["jwtInfo"])</log>
</inbound>
Related Topics
- Policy Engine — Advanced policies
- Rate Limiting — Throttling
- Caching — Response caching
Azure Integration Hub - Intermediate Level