Skip to content

Commit 3b5acdf

Browse files
Add phase 4 dynamic-cors
1 parent 01490df commit 3b5acdf

3 files changed

Lines changed: 368 additions & 36 deletions

File tree

samples/dynamic-cors/README.md

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Implement dynamic, per-API CORS origin validation in Azure API Management using
1010

1111
1. Understand why the built-in APIM `<cors>` policy cannot support fully dynamic origin validation and how to replace it with custom policy fragments.
1212
1. 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.
13-
1. Compare four mapping strategies side-by-side: **native `<cors>` policy** (Baseline), **hard-coded** (Phase 1), **Named Values** (Phase 2), and **cache-backed** (Phase 3), understanding the trade-offs of each.
13+
1. Compare five mapping strategies side-by-side: **native `<cors>` policy** (Baseline), **hard-coded** (Phase 1), **Named Values** (Phase 2), **cache-backed** (Phase 3), and **per-API cache** (Phase 4), understanding the trade-offs of each.
1414
1. Use an admin API (`/admin/load-cache`) to populate the APIM internal cache at runtime, demonstrating the `/admin/` convention for operational endpoints.
1515
1. Verify CORS behaviour with automated tests covering allowed origins, disallowed origins, missing `Origin` headers, and fail-closed cache behaviour.
1616

@@ -29,52 +29,58 @@ You need a single, reusable CORS mechanism that can be applied to any API while
2929

3030
This lab deploys all phases **side-by-side** so you can inspect and compare them without redeployment:
3131

32-
- **Nine APIs** (two per phase 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.
32+
- **Eleven APIs** (two per phase 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.
3333
- **Baseline** (`cors-bl-products`, `cors-bl-analytics`) - native APIM `<cors>` policy with static origins.
3434
- **Phase 1** (`cors-ph1-products`, `cors-ph1-analytics`) - `DynamicCorsHardcoded` policy fragment.
3535
- **Phase 2** (`cors-ph2-products`, `cors-ph2-analytics`) - `DynamicCorsNamedValues` policy fragment.
36-
- **Phase 3** (`cors-ph3-products`, `cors-ph3-analytics`) - `DynamicCorsCached` policy fragment.
36+
- **Phase 3** (`cors-ph3-products`, `cors-ph3-analytics`) - `DynamicCorsCached` policy fragment (single cache entry for all APIs).
37+
- **Phase 4** (`cors-ph4-products`, `cors-ph4-analytics`) - `DynamicCorsCachedPerApi` policy fragment (per-API cache entries).
3738
- **Admin** (`cors-admin`) - `POST /load-cache/{cacheKey}` stores a value in the APIM internal cache and `POST /clear-cache/{cacheKey}` removes it (subscription required).
3839

3940
> [!IMPORTANT]
4041
> **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](../authX/) and [authX-pro](../authX-pro/) samples for implementation patterns. The policy XML includes a commented example of where to place the validation.
4142
42-
- **Three APIM policy fragments** (one per dynamic phase) demonstrating progressively more flexible origin-mapping strategies:
43+
- **Four APIM policy fragments** (one per dynamic phase) demonstrating progressively more flexible origin-mapping strategies:
4344
- `DynamicCorsHardcoded` - origins embedded in a C# `switch` expression.
4445
- `DynamicCorsNamedValues` - origins read from an APIM Named Value as JSON.
45-
- `DynamicCorsCached` - origins read from the APIM internal cache. Returns `503` if the cache is not initialized (fail-closed).
46+
- `DynamicCorsCached` - origins read from the APIM internal cache as a single JSON mapping. Returns `503` if the cache is not initialized (fail-closed).
47+
- `DynamicCorsCachedPerApi` - origins read from per-API cache entries (`corsOriginMapping-{apiId}`). Returns `503` if the current API's cache entry is missing (fail-closed).
4648
- **One Named Value** (`CorsOriginMapping`) holding the JSON origin mapping for Phase 2.
4749
- 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.
4850

4951
### Progression
5052

51-
| Phase | Policy | Mapping location | Trade-offs |
52-
| ------------ | ----------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------- |
53-
| **Baseline** | Native `<cors>` | Static XML attribute list | Same origins for all APIs; cannot vary per API |
54-
| **Phase 1** | `DynamicCorsHardcoded` fragment | Inline `switch/case` in C# | Per-API control; requires redeploying the fragment to change origins |
55-
| **Phase 2** | `DynamicCorsNamedValues` fragment | JSON string in a Named Value | Updateable in the portal; **4,096-char limit** per Named Value |
56-
| **Phase 3** | `DynamicCorsCached` fragment + admin API | APIM internal cache | No size limit; updated via admin API; fail-closed when cache is empty; can swap to external Redis |
53+
| Phase | Policy | Mapping location | Trade-offs |
54+
| ------------ | ------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------- |
55+
| **Baseline** | Native `<cors>` | Static XML attribute list | Same origins for all APIs; cannot vary per API |
56+
| **Phase 1** | `DynamicCorsHardcoded` fragment | Inline `switch/case` in C# | Per-API control; requires redeploying the fragment to change origins |
57+
| **Phase 2** | `DynamicCorsNamedValues` fragment | JSON string in a Named Value | Updateable in the portal; **4,096-char limit** per Named Value |
58+
| **Phase 3** | `DynamicCorsCached` fragment + admin API | APIM internal cache (single entry) | No size limit; updated via admin API; fail-closed when cache is empty; can swap to external Redis |
59+
| **Phase 4** | `DynamicCorsCachedPerApi` fragment + admin API | APIM internal cache (per-API entry) | Per-API cache isolation; smaller cache reads; update one API without touching others |
5760

5861
### Comparison Matrix
5962

60-
| Criterion | Baseline | Phase 1 | Phase 2 | Phase 3 |
61-
| ------------------------------------------- | :------: | :-----: | :-----: | :-----: |
62-
| Per-API origin control | - | + | + | + |
63-
| No fragment redeployment to change origins | + | - | + | + |
64-
| No size limit on origin mapping | + | + | - | + |
65-
| Zero additional infrastructure | + | + | + | - |
66-
| Update origins without Azure portal access | n/a | - | - | + |
67-
| Fail-closed when mapping is absent | n/a | n/a | n/a | + |
68-
| Observability (trace logging) | - | + | + | + |
69-
| Swap to external Redis without code changes | n/a | n/a | n/a | + |
70-
| Complexity | Low | Low | Low | Medium |
63+
| Criterion | Baseline | Phase 1 | Phase 2 | Phase 3 | Phase 4 |
64+
| ------------------------------------------- | :------: | :-----: | :-----: | :-----: | :-----: |
65+
| Per-API origin control | - | + | + | + | + |
66+
| No fragment redeployment to change origins | + | - | + | + | + |
67+
| No size limit on origin mapping | + | + | - | + | + |
68+
| Zero additional infrastructure | + | + | + | - | - |
69+
| Update origins without Azure portal access | n/a | - | - | + | + |
70+
| Fail-closed when mapping is absent | n/a | n/a | n/a | + | + |
71+
| Observability (trace logging) | - | + | + | + | + |
72+
| Swap to external Redis without code changes | n/a | n/a | n/a | + | + |
73+
| Update single API without full cache reload | n/a | n/a | n/a | - | + |
74+
| Smaller per-request cache reads | n/a | n/a | n/a | - | + |
75+
| Complexity | Low | Low | Low | Medium | Medium |
7176

7277
**Legend:** `+` = advantage, `-` = limitation, `n/a` = not applicable to this approach.
7378

7479
- **Baseline** is the simplest starting point but cannot differentiate origins per API.
7580
- **Phase 1** adds per-API control with zero infrastructure overhead, ideal for a small, stable set of origins.
7681
- **Phase 2** removes the need to redeploy fragments when origins change, but is constrained by the 4,096-character Named Value limit.
7782
- **Phase 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.
83+
- **Phase 4** builds on Phase 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 Phase 3 plus the need to load each API's cache entry individually.
7884

7985
## ⚙️ Configuration
8086

0 commit comments

Comments
 (0)