- Azure API Management Policies and Custom Authentication Flows – Part 1: Fundamentals
- Azure API Management Policies and Custom Authentication Flows – Part 2: Authentication & Authorization Deep Dive
- Azure API Management Policies and Custom Authentication Flows – Part 3: Custom Authentication Implementation
- Azure API Management Policies and Custom Authentication Flows – Part 4: Advanced Scenarios & Best Practices
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:
Scope | Description | Use Cases | Inheritance |
---|---|---|---|
Global | Applies to all APIs | Common security, logging, CORS | Base level |
Product | Applies to APIs in a product | Subscription validation, rate limits | Inherits from Global |
API | Applies to specific API | API-specific authentication, caching | Inherits from Product |
Operation | Applies to specific operation | Fine-grained transformations | Inherits 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.