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

ClaimDescription
issIssuer - who created the token
subSubject - who the token is about
audAudience - intended recipient
expExpiration time
iatIssued at time
rolesRoles assigned to user
scpOAuth 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

  1. Go to Azure Portal → Microsoft Entra ID
  2. Register a new application (App registrations)
  3. Note the Application (client) ID
  4. Note the Directory (tenant) ID

Step 2: Expose API

  1. In your app registration, go to "Expose an API"
  2. Set Application ID URI
  3. 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

PracticeDescription
Use OpenID ConfigLet APIM fetch signing keys automatically
Validate audienceEnsure token is for your API
Check expirationBuilt-in, but verify TTL is reasonable
Require necessary claimsAdd role/permission checks
Extract claimsPass user info to backend
Handle errors gracefullyReturn 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

ErrorCauseSolution
IDX10500Signature validation failedCheck signing key
IDX10501Audience validation failedAdd correct audience
IDX10502Issuer validation failedAdd correct issuer
IDX10503Expiration time validation failedToken expired
IDX10504Required claim missingAdd 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


Azure Integration Hub - Intermediate Level