|
| 1 | +<!-- |
| 2 | + Policy fragment: Dynamic CORS (Named Value, per-API) |
| 3 | +
|
| 4 | + Evaluates the Origin header against a JSON array passed in via a context variable. |
| 5 | + The calling API policy must set 'allowedOriginsJson' before including this fragment, |
| 6 | + typically by referencing a per-API Named Value: |
| 7 | +
|
| 8 | + <set-variable name="allowedOriginsJson" value="{{CorsOrigins-my-api-id}}" /> |
| 9 | + <include-fragment fragment-id="DynamicCorsNvPerApi" /> |
| 10 | +
|
| 11 | + Expected context variable format: |
| 12 | + allowedOriginsJson = '["https://origin-a.com","https://origin-b.com"]' |
| 13 | +
|
| 14 | + This pattern makes the fragment completely environment-agnostic. The same fragment |
| 15 | + works across all environments; only the Named Value content differs per deployment. |
| 16 | + Each API controls its own origins independently, with no character-limit concerns |
| 17 | + from a shared mapping object. |
| 18 | +
|
| 19 | + Context variables expected: |
| 20 | + allowedOriginsJson - JSON array of allowed origins (set by the calling API policy). |
| 21 | +
|
| 22 | + Context variables set: |
| 23 | + corsOrigin - The validated origin if allowed, or empty string if not. |
| 24 | +--> |
| 25 | +<fragment> |
| 26 | + <!-- Evaluate the Origin header against the per-API origin array from the Named Value --> |
| 27 | + <set-variable name="corsOrigin" value="@{ |
| 28 | + var origin = context.Request.Headers.GetValueOrDefault("Origin", ""); |
| 29 | + if (string.IsNullOrEmpty(origin)) { return ""; } |
| 30 | +
|
| 31 | + var allowedOriginsJson = context.Variables.GetValueOrDefault<string>("allowedOriginsJson", "[]"); |
| 32 | + var allowedOrigins = JArray.Parse(allowedOriginsJson); |
| 33 | +
|
| 34 | + if (allowedOrigins.Any(o => o.ToString() == "*")) { return origin; } |
| 35 | + if (allowedOrigins.Any(o => o.ToString() == origin)) { return origin; } |
| 36 | +
|
| 37 | + return ""; |
| 38 | + }" /> |
| 39 | + <choose> |
| 40 | + <when condition="@(context.Request.Method == "OPTIONS")"> |
| 41 | + <choose> |
| 42 | + <when condition="@(!string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("corsOrigin", "")))"> |
| 43 | + <return-response> |
| 44 | + <set-status code="200" reason="OK" /> |
| 45 | + <set-header name="Access-Control-Allow-Origin" exists-action="override"> |
| 46 | + <value>@((string)context.Variables["corsOrigin"])</value> |
| 47 | + </set-header> |
| 48 | + <set-header name="Access-Control-Allow-Methods" exists-action="override"> |
| 49 | + <value>GET, POST, OPTIONS</value> |
| 50 | + </set-header> |
| 51 | + <set-header name="Access-Control-Allow-Headers" exists-action="override"> |
| 52 | + <value>Content-Type, Authorization, X-Requested-With</value> |
| 53 | + </set-header> |
| 54 | + <set-header name="Access-Control-Max-Age" exists-action="override"> |
| 55 | + <value>86400</value> |
| 56 | + </set-header> |
| 57 | + </return-response> |
| 58 | + </when> |
| 59 | + <otherwise> |
| 60 | + <trace source="DynamicCorsNvPerApi" severity="information"> |
| 61 | + <message>CORS origin rejected by per-API Named Value mapping.</message> |
| 62 | + <metadata name="apiId" value="@(context.Api.Id)" /> |
| 63 | + <metadata name="operationId" value="@(context.Operation.Id)" /> |
| 64 | + <metadata name="origin" value="@(context.Request.Headers.GetValueOrDefault("Origin", ""))" /> |
| 65 | + </trace> |
| 66 | + <return-response> |
| 67 | + <set-status code="403" reason="CORS Origin Not Allowed" /> |
| 68 | + <set-header name="Content-Type" exists-action="override"> |
| 69 | + <value>application/json</value> |
| 70 | + </set-header> |
| 71 | + <set-body>{"error": "CORS origin not allowed for this API"}</set-body> |
| 72 | + </return-response> |
| 73 | + </otherwise> |
| 74 | + </choose> |
| 75 | + </when> |
| 76 | + </choose> |
| 77 | +</fragment> |
0 commit comments