Azure API Management Policies and Custom Authentication Flows – Part 1: Fundamentals

Azure API Management Policies and Custom Authentication Flows – Part 1: Fundamentals

Azure API Management Policies and Custom Authentication Flows

Part 1: Azure API Management Policies Fundamentals

Azure API Management (APIM) policies are the powerhouse behind API gateway functionality, enabling you to modify API behavior without changing backend code. Understanding policies is crucial for implementing robust authentication, rate limiting, caching, and transformation logic.

Policy Framework Architecture

Policies in Azure API Management follow a pipeline execution model with four distinct sections that execute at different stages of the request-response cycle.

Policy Execution Pipeline

<policies>
    <inbound>
        <!-- Executes on incoming requests before forwarding to backend -->
        <rate-limit calls="100" renewal-period="60" />
        <validate-jwt header-name="Authorization" failed-validation-httpcode="401">
            <openid-config url="https://login.microsoftonline.com/common/.well-known/openid_configuration" />
        </validate-jwt>
        <set-header name="X-Forwarded-For" exists-action="override">
            <value>@(context.Request.IpAddress)</value>
        </set-header>
        <base />
    </inbound>
    
    <backend>
        <!-- Executes before/after backend call -->
        <retry condition="@(context.Response.StatusCode == 503)" count="3" interval="1" />
        <base />
    </backend>
    
    <outbound>
        <!-- Executes on responses before returning to client -->
        <remove-header name="X-Powered-By" />
        <set-header name="X-Response-Time" exists-action="override">
            <value>@(context.Elapsed.TotalMilliseconds.ToString())</value>
        </set-header>
        <base />
    </outbound>
    
    <on-error>
        <!-- Executes when errors occur -->
        <send-request mode="new" response-variable-name="errorlog" timeout="10" ignore-error="true">
            <set-url>https://logging-service.com/api/errors</set-url>
            <set-method>POST</set-method>
            <set-body>@{
                return new JObject(
                    new JProperty("timestamp", DateTime.UtcNow),
                    new JProperty("error", context.LastError.Message),
                    new JProperty("requestId", context.RequestId)
                ).ToString();
            }</set-body>
        </send-request>
        <base />
    </on-error>
</policies>

Policy Scopes and Inheritance

Azure API Management applies policies at multiple levels with a clear inheritance hierarchy:

ScopeDescriptionUse CasesInheritance
GlobalApplies to all APIsCommon security, logging, CORSBase level
ProductApplies to APIs in a productSubscription validation, rate limitsInherits from Global
APIApplies to specific APIAPI-specific authentication, cachingInherits from Product
OperationApplies to specific operationFine-grained transformationsInherits from API

Policy Inheritance Example

<!-- Global Policy (applies to all APIs) -->
<policies>
    <inbound>
        <cors allow-credentials="false">
            <allowed-origins>
                <origin>https://contoso.com</origin>
            </allowed-origins>
            <allowed-methods>
                <method>GET</method>
                <method>POST</method>
            </allowed-methods>
        </cors>
        <base />
    </inbound>
</policies>

<!-- API-Level Policy (inherits CORS, adds rate limiting) -->
<policies>
    <inbound>
        <rate-limit-by-key calls="100" renewal-period="60" 
            counter-key="@(context.Request.IpAddress)" />
        <base /> <!-- Includes global CORS policy -->
    </inbound>
</policies>

<!-- Operation-Level Policy (inherits both, adds transformation) -->
<policies>
    <inbound>
        <set-query-parameter name="version" exists-action="override">
            <value>v2</value>
        </set-query-parameter>
        <base /> <!-- Includes rate limiting and CORS -->
    </inbound>
</policies>

Essential Built-in Policies

1. Rate Limiting and Throttling

<!-- Basic rate limiting -->
<rate-limit calls="1000" renewal-period="3600" />

<!-- Advanced rate limiting by subscription key -->
<rate-limit-by-key calls="100" renewal-period="60" 
    counter-key="@(context.Subscription?.Key ?? "anonymous")" />

<!-- Rate limiting with custom key and headers -->
<rate-limit-by-key calls="50" renewal-period="60" 
    counter-key="@(context.Request.IpAddress + ":" + context.Request.Headers.GetValueOrDefault("User-Agent", ""))"
    remaining-calls-header-name="X-RateLimit-Remaining"
    total-calls-header-name="X-RateLimit-Limit"
    retry-after-header-name="X-RateLimit-RetryAfter" />

<!-- Quota enforcement over longer periods -->
<quota-by-key calls="10000" renewal-period="2592000" 
    counter-key="@(context.Subscription.Key)" />

2. Caching Policies

<!-- Response caching in outbound section -->
<outbound>
    <cache-store duration="3600" />
</outbound>

<!-- Conditional caching based on response -->
<outbound>
    <cache-store duration="@(
        context.Response.StatusCode == 200 ? 3600 : 60
    )" vary-by-header="Accept-Language" />
</outbound>

<!-- Cache lookup in inbound section -->
<inbound>
    <cache-lookup vary-by-developer="true" 
        vary-by-developer-groups="true"
        downstream-caching-type="none" />
</inbound>

<!-- Advanced caching with custom key -->
<inbound>
    <cache-lookup-value key="@("user-profile-" + context.Request.Headers.GetValueOrDefault("UserId", ""))" 
        variable-name="userProfile" />
    <choose>
        <when condition="@(!context.Variables.ContainsKey("userProfile"))">
            <!-- Fetch from backend and cache -->
        </when>
        <otherwise>
            <return-response>
                <set-status code="200" />
                <set-body>@((string)context.Variables["userProfile"])</set-body>
            </return-response>
        </otherwise>
    </choose>
</inbound>

3. Request/Response Transformation

<!-- Header manipulation -->
<set-header name="X-API-Version" exists-action="override">
    <value>2.0</value>
</set-header>

<set-header name="X-User-Context" exists-action="override">
    <value>@{
        var jwt = context.Request.Headers.GetValueOrDefault("Authorization", "").Replace("Bearer ", "");
        if (!string.IsNullOrEmpty(jwt))
        {
            var payload = jwt.Split('.')[1];
            var decoded = Convert.FromBase64String(payload + new string('=', (4 - payload.Length % 4) % 4));
            var json = Encoding.UTF8.GetString(decoded);
            var token = JObject.Parse(json);
            return token["sub"]?.ToString() ?? "anonymous";
        }
        return "anonymous";
    }</value>
</set-header>

<!-- Query parameter transformation -->
<set-query-parameter name="apikey" exists-action="delete" />
<set-query-parameter name="timestamp" exists-action="override">
    <value>@(DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"))</value>
</set-query-parameter>

<!-- Request body transformation -->
<set-body>@{
    var body = context.Request.Body.As<JObject>();
    body["processedAt"] = DateTime.UtcNow;
    body["apiVersion"] = "v2";
    return body.ToString();
}</set-body>

<!-- Response transformation -->
<outbound>
    <set-body>@{
        var response = context.Response.Body.As<JObject>();
        return new JObject(
            new JProperty("success", true),
            new JProperty("data", response),
            new JProperty("timestamp", DateTime.UtcNow),
            new JProperty("requestId", context.RequestId)
        ).ToString();
    }</set-body>
</outbound>

Policy Expressions and Context Variables

Understanding the Context Object

<!-- Accessing request information -->
@(context.Request.Method)           // GET, POST, etc.
@(context.Request.Url.Path)         // /api/users
@(context.Request.Url.Query)        // Query string parameters
@(context.Request.IpAddress)        // Client IP address
@(context.Request.Headers["User-Agent"]) // Specific header

<!-- Accessing user and subscription info -->
@(context.User?.Id)                 // User identifier
@(context.User?.Email)              // User email
@(context.Subscription?.Key)        // Subscription key
@(context.Subscription?.Name)       // Subscription name
@(context.Product?.Name)            // Product name

<!-- Working with JWT tokens -->
@{
    string authHeader = context.Request.Headers.GetValueOrDefault("Authorization", "");
    if (authHeader.StartsWith("Bearer "))
    {
        string token = authHeader.Substring("Bearer ".Length);
        // Parse JWT token
        string[] parts = token.Split('.');
        if (parts.Length == 3)
        {
            string payload = parts[1];
            // Add padding if necessary
            payload += new string('=', (4 - payload.Length % 4) % 4);
            byte[] data = Convert.FromBase64String(payload);
            string json = Encoding.UTF8.GetString(data);
            JObject claims = JObject.Parse(json);
            return claims["sub"]?.ToString();
        }
    }
    return "anonymous";
}

<!-- Advanced expression with error handling -->
@{
    try
    {
        var body = context.Request.Body.As<JObject>();
        var userId = body["userId"]?.ToString();
        
        if (string.IsNullOrEmpty(userId))
        {
            throw new ArgumentException("UserId is required");
        }
        
        // Validate user ID format
        if (!Guid.TryParse(userId, out Guid userGuid))
        {
            throw new ArgumentException("UserId must be a valid GUID");
        }
        
        return userGuid.ToString();
    }
    catch (Exception ex)
    {
        context.Variables["error"] = ex.Message;
        return null;
    }
}

Working with Variables

<!-- Setting variables -->
<set-variable name="userId" value="@(context.Request.Headers.GetValueOrDefault("X-User-ID", ""))" />
<set-variable name="isAdmin" value="@(context.User?.Groups?.Contains("administrators") == true)" />

<!-- Using variables in conditions -->
<choose>
    <when condition="@((bool)context.Variables["isAdmin"])">
        <set-header name="X-Admin-Access" exists-action="override">
            <value>true</value>
        </set-header>
    </when>
    <otherwise>
        <rate-limit calls="100" renewal-period="3600" />
    </otherwise>
</choose>

<!-- Complex variable manipulation -->
<set-variable name="requestMetadata" value="@{
    return new JObject(
        new JProperty("timestamp", DateTime.UtcNow),
        new JProperty("method", context.Request.Method),
        new JProperty("path", context.Request.Url.Path),
        new JProperty("userAgent", context.Request.Headers.GetValueOrDefault("User-Agent", "")),
        new JProperty("requestId", context.RequestId)
    );
}" />

Error Handling and Debugging

Policy Error Handling

<!-- Global error handling -->
<on-error>
    <set-variable name="errorDetails" value="@{
        return new JObject(
            new JProperty("timestamp", DateTime.UtcNow),
            new JProperty("requestId", context.RequestId),
            new JProperty("error", new JObject(
                new JProperty("message", context.LastError?.Message),
                new JProperty("source", context.LastError?.Source),
                new JProperty("reason", context.LastError?.Reason)
            ))
        );
    }" />
    
    <!-- Log error to external service -->
    <send-request mode="new" response-variable-name="errorlog" timeout="5" ignore-error="true">
        <set-url>https://logging.contoso.com/api/errors</set-url>
        <set-method>POST</set-method>
        <set-header name="Content-Type" exists-action="override">
            <value>application/json</value>
        </set-header>
        <set-body>@(((JObject)context.Variables["errorDetails"]).ToString())</set-body>
    </send-request>
    
    <!-- Return user-friendly error -->
    <return-response>
        <set-status code="500" reason="Internal Server Error" />
        <set-header name="Content-Type" exists-action="override">
            <value>application/json</value>
        </set-header>
        <set-body>@{
            return new JObject(
                new JProperty("error", new JObject(
                    new JProperty("code", "INTERNAL_ERROR"),
                    new JProperty("message", "An unexpected error occurred"),
                    new JProperty("requestId", context.RequestId)
                ))
            ).ToString();
        }</set-body>
    </return-response>
</on-error>

Policy Testing and Debugging

<!-- Debug headers for development -->
<outbound>
    <choose>
        <when condition="@(context.Request.Headers.GetValueOrDefault("X-Debug-Mode", "") == "true")">
            <set-header name="X-Debug-Request-Id" exists-action="override">
                <value>@(context.RequestId)</value>
            </set-header>
            <set-header name="X-Debug-User-Id" exists-action="override">
                <value>@(context.User?.Id ?? "anonymous")</value>
            </set-header>
            <set-header name="X-Debug-Backend-Time" exists-action="override">
                <value>@(context.Response.Headers.GetValueOrDefault("X-Response-Time", "unknown"))</value>
            </set-header>
            <set-header name="X-Debug-Policy-Time" exists-action="override">
                <value>@(context.Elapsed.TotalMilliseconds.ToString())</value>
            </set-header>
        </when>
    </choose>
</outbound>

<!-- Trace logging for debugging -->
<inbound>
    <trace source="policy-debug">
        <message>@{
            return $"Request: {context.Request.Method} {context.Request.Url.Path} from {context.Request.IpAddress}";
        }</message>
        <metadata name="requestId" value="@(context.RequestId)" />
        <metadata name="userId" value="@(context.User?.Id ?? "anonymous")" />
    </trace>
</inbound>

Performance Optimization

Policy Performance Best Practices

  • Minimize policy complexity: Keep expressions simple and avoid heavy computations
  • Use caching effectively: Cache expensive operations and external calls
  • Order policies efficiently: Place quick-fail policies (auth, rate limiting) early
  • Optimize external calls: Use short timeouts and proper error handling
<!-- Efficient external service call with caching -->
<inbound>
    <cache-lookup-value key="@("user-permissions-" + context.User?.Id)" 
        variable-name="userPermissions" />
    
    <choose>
        <when condition="@(!context.Variables.ContainsKey("userPermissions"))">
            <send-request mode="new" response-variable-name="permissionResponse" timeout="2">
                <set-url>@($"https://auth.contoso.com/api/users/{context.User?.Id}/permissions")</set-url>
                <set-method>GET</set-method>
                <set-header name="Authorization" exists-action="override">
                    <value>Bearer {{service-token}}</value>
                </set-header>
            </send-request>
            
            <set-variable name="userPermissions" 
                value="@(((IResponse)context.Variables["permissionResponse"]).Body.As<string>())" />
            
            <cache-store-value key="@("user-permissions-" + context.User?.Id)" 
                value="@((string)context.Variables["userPermissions"])" duration="900" />
        </when>
    </choose>
</inbound>

What’s Coming Next

In Part 2, we’ll dive deep into authentication and authorization policies, exploring JWT validation, OAuth 2.0 integration, certificate-based authentication, and API key management strategies.

In Part 3, we’ll implement custom authentication flows with practical examples of multi-factor authentication, external identity provider integration, and custom token validation.

In Part 4, we’ll cover advanced scenarios including B2B authentication, policy testing frameworks, performance optimization, and enterprise security best practices.


Azure API Management policies provide the foundation for building secure, scalable, and maintainable API gateways. Master the fundamentals covered in this part, and you’ll be ready to implement sophisticated authentication and authorization schemes in the upcoming parts of this series.

NavigateAzure API Management Policies and Custom Authentication Flows – Part 2: Authentication & Authorization Deep Dive >>

Written by:

265 Posts

View All Posts
Follow Me :