Samples: Dynamic CORS

April 9, 2026 ยท View on GitHub

Implement dynamic, per-API CORS origin validation in Azure API Management using custom policy fragments instead of the built-in <cors> policy. The built-in policy requires a static list of allowed origins at deployment time and its <origin> elements do not support policy expressions, Named Values, or context variables. This sample shows how to evaluate origins dynamically at runtime with a maintainable mapping of API ID to allowed origins.

โš™๏ธ Supported infrastructures: All infrastructures

๐Ÿ‘Ÿ Expected Run All runtime (excl. infrastructure prerequisite): ~5 minutes

๐ŸŽฏ Objectives

  1. Understand why the built-in APIM <cors> policy cannot support fully dynamic origin validation and how to replace it with custom policy fragments.
  2. Build a reusable policy fragment that evaluates the Origin header against a per-API allowed-origins mapping, handling both OPTIONS preflight and actual request CORS headers.
  3. Compare six mapping strategies side-by-side: native <cors> policy (Baseline), hard-coded (Option 1), Named Values (Option 2), cache-backed (Option 3), per-API cache (Option 4), and per-API Named Values via context variables (Option 5), understanding the trade-offs of each.
  4. Use an admin API (/admin/load-cache) to populate the APIM internal cache at runtime, demonstrating the /admin/ convention for operational endpoints.
  5. Verify CORS behaviour with automated tests covering allowed origins, disallowed origins, missing Origin headers, and fail-closed cache behaviour.

๐Ÿ“ Scenario

Your organisation exposes multiple APIs through APIM. Different APIs serve different frontends:

APIAllowed OriginsRationale
Productshttps://shop.contoso.com, https://admin.contoso.comOnly the shop and admin portals may call this API.
Analyticshttps://dashboard.contoso.comOnly the analytics dashboard may call this API.

You need a single, reusable CORS mechanism that can be applied to any API while keeping the per-API origin configuration easy to maintain.

๐Ÿ›ฉ๏ธ Lab Components

This lab deploys all options side-by-side so you can inspect and compare them without redeployment:

  • Thirteen APIs (two per option plus an admin API) with no backends. Each CORS demo API includes a GET operation returning a JSON response indicating whether CORS was allowed and an OPTIONS operation for preflight handling.
    • Baseline (cors-bl-products, cors-bl-analytics) - native APIM <cors> policy with static origins.
    • Option 1 (cors-opt1-products, cors-opt1-analytics) - DynamicCorsHardcoded policy fragment.
    • Option 2 (cors-opt2-products, cors-opt2-analytics) - DynamicCorsNamedValues policy fragment.
    • Option 3 (cors-opt3-products, cors-opt3-analytics) - DynamicCorsCached policy fragment (single cache entry for all APIs).
    • Option 4 (cors-opt4-products, cors-opt4-analytics) - DynamicCorsCachedPerApi policy fragment (per-API cache entries).
    • Option 5 (cors-opt5-products, cors-opt5-analytics) - DynamicCorsNvPerApi policy fragment (per-API Named Values passed via context variable).
    • Admin (cors-admin) - POST /load-cache/{cacheKey} stores a value in the APIM internal cache and POST /clear-cache/{cacheKey} removes it (subscription required).

Important

Production security: The admin API in this sample is protected by a subscription key only. Subscription keys are shared secrets and are not a substitute for identity-based authentication. In production, you should add validate-azure-ad-token or validate-jwt to the admin API's inbound policy. See the authX and authX-pro samples for implementation patterns. The policy XML includes a commented example of where to place the validation.

  • Five APIM policy fragments (one per dynamic option) demonstrating different origin-mapping strategies:
    • DynamicCorsHardcoded - origins embedded in a C# switch expression.
    • DynamicCorsNamedValues - origins read from an APIM Named Value as JSON.
    • DynamicCorsCached - origins read from the APIM internal cache as a single JSON mapping. Returns 503 if the cache is not initialized (fail-closed).
    • DynamicCorsCachedPerApi - origins read from per-API cache entries (corsOriginMapping-{apiId}). Returns 503 if the current API's cache entry is missing (fail-closed).
    • DynamicCorsNvPerApi - origins passed via a context variable set by the API-level policy from a per-API Named Value. The fragment itself is environment-agnostic.
  • Three Named Values: CorsOriginMapping (Option 2 JSON mapping), CorsOrigins-cors-opt5-products and CorsOrigins-cors-opt5-analytics (Option 5 per-API origin arrays).
  • An API-level policy (cors-api-policy.xml) that includes the active CORS fragment in <inbound> and documents the outbound pattern for APIs with real backends.
  • A context-variable API-level policy (cors-api-policy-named-values.xml) that sets an allowedOriginsJson context variable from a Named Value reference before including the Option 5 fragment.

Options

OptionPolicyMapping locationTrade-offs
BaselineNative <cors>Static XML attribute listSame origins for all APIs; cannot vary per API
Option 1DynamicCorsHardcoded fragmentInline switch/case in C#Per-API control; requires redeploying the fragment to change origins
Option 2DynamicCorsNamedValues fragmentJSON string in a Named ValueUpdateable in the portal; 4,096-char limit per Named Value
Option 3DynamicCorsCached fragment + admin APIAPIM internal cache (single entry)No size limit; updated via admin API; fail-closed when cache is empty; can swap to external Redis
Option 4DynamicCorsCachedPerApi fragment + admin APIAPIM internal cache (per-API entry)Per-API cache isolation; smaller cache reads; update one API without touching others
Option 5DynamicCorsNvPerApi fragmentPer-API Named Value via context varEnvironment-agnostic fragment; no cache warm-up; origins available at deploy time

Comparison Matrix

CriterionBaselineOption 1Option 2Option 3Option 4Option 5
Per-API origin controlโŒโœ…โœ…โœ…โœ…โœ…
No fragment redeployment to change originsโœ…โŒโœ…โœ…โœ…โœ…
No size limit on origin mappingโœ…โœ…โŒโœ…โœ…โŒ
Zero additional infrastructureโœ…โœ…โœ…โŒโŒโœ…
Update origins via APIโž–โŒโŒโœ…โœ…โŒ
Fail-closed when mapping is absentโž–โž–โž–โœ…โœ…โž–
Observability (trace logging)โŒโœ…โœ…โœ…โœ…โœ…
Swap to external Redis without code changesโž–โž–โž–โœ…โœ…โž–
Update single API without full cache reloadโž–โž–โž–โŒโœ…โœ…
Smaller per-request cache readsโž–โž–โž–โŒโœ…โž–
Environment-agnostic fragmentโž–โŒโŒโŒโŒโœ…
Origins available immediately at deployโœ…โœ…โœ…โŒโŒโœ…
ComplexityLowLowLowMediumMediumLow

Legend: โœ… = advantage, โŒ = limitation, โž– = not applicable to this approach.

  • Baseline is the simplest starting point but cannot differentiate origins per API.
  • Option 1 adds per-API control with zero infrastructure overhead, ideal for a small, stable set of origins.
  • Option 2 removes the need to redeploy fragments when origins change, but is constrained by the 4,096-character Named Value limit.
  • Option 3 lifts all size limits, enables runtime updates via an admin API, and adopts a fail-closed posture. The trade-off is the additional admin API surface and the requirement to initialise the cache after an APIM restart or scale-out.
  • Option 4 builds on Option 3 by storing each API's origins in a separate cache entry (corsOriginMapping-{apiId}). This means each request reads only its own API's origin array (smaller payload), and updating one API's origins does not require reloading the entire mapping. The trade-off is the same as Option 3 plus the need to load each API's cache entry individually.
  • Option 5 takes a different approach: each API's policy sets a context variable (allowedOriginsJson) from its own Named Value (CorsOrigins-{apiId}) before including a shared, environment-agnostic fragment. The fragment has no knowledge of where the data comes from. This mirrors the pattern used by the authX-pro sample. Origins are available immediately at deployment time with no cache warm-up. The trade-off is the same 4,096-character Named Value limit as Option 2 (per API, not shared), and updating origins requires portal or CLI access.

โš™๏ธ Configuration

  1. Decide which of the Infrastructure Architectures you wish to use.
    1. If the infrastructure does not yet exist, navigate to the desired infrastructure folder and follow its README.md.
    2. If the infrastructure does exist, adjust the user-defined parameters in the Initialize notebook variables below. Please ensure that all parameters match your infrastructure.

๐Ÿ”— Additional Resources