From fed0be0098f6f22a559ea1e21a1c7c41af521e88 Mon Sep 17 00:00:00 2001 From: MarvelintheCloud Date: Wed, 20 May 2026 23:47:59 -0500 Subject: [PATCH 1/6] Add POC: Security and OAuth 2.0 Configuration --- docs/POC-Security-OAuth-Configuration.md | 166 +++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 docs/POC-Security-OAuth-Configuration.md diff --git a/docs/POC-Security-OAuth-Configuration.md b/docs/POC-Security-OAuth-Configuration.md new file mode 100644 index 0000000..42cddc9 --- /dev/null +++ b/docs/POC-Security-OAuth-Configuration.md @@ -0,0 +1,166 @@ +# POC: Security and OAuth 2.0 Configuration + +**Purpose:** Validate the repo-aligned OAuth 2.0 trust chain across client -> Container App -> APIM, including the three Entra app registrations and Graph-based app role assignments. + +## TL;DR (< 5 minutes) + +1. **Most important rule: each hop must use its own token audience (`aud`) and role check; do not pass the same token end-to-end.** +2. Create three app registrations: APIM protected API, ACA protected API, and client caller app. +3. Assign app roles with Microsoft Graph PowerShell when portal UI cannot target managed identities. + +## What you will observe + +- Client obtains a token for ACA (`aud = api://`) and can call ACA when ACA auth is enabled. +- ACA uses managed identity to obtain a separate token for APIM (`aud = api://`) and APIM accepts `roles = API.Caller`. +- App role assignment succeeds for identities not selectable in portal UI by using Graph PowerShell cmdlets. + +## Reference + +| Setting | Value in this POC | Unit | Set in | Takes effect | +| :--- | :--- | :--- | :--- | :--- | +| APIM API app registration | `SimpleL7Proxy-APIM-API` | name | Entra App registrations | immediate | +| ACA API app registration | `SimpleL7Proxy-ACA-API` | name | Entra App registrations | immediate | +| Client app registration | `SimpleL7Proxy-Client` | name | Entra App registrations | immediate | +| App role value | `API.Caller` | claim value | APIM API + ACA API app registrations | token issuance | +| ACA scope value | `api.access` | scope value | ACA API app registration | token issuance | +| APIM audience | `api://` | URI | APIM `validate-jwt` policy | policy save | +| ACA audience | `api://` | URI | ACA auth config | config save | +| Client -> ACA token resource | `api://` | URI | token request | per request | +| ACA -> APIM token resource | `api:///.default` | URI | managed identity token request | per request | +| Graph module permission | `AppRoleAssignment.ReadWrite.All` | Graph scope | `Connect-MgGraph` | login session | + +> [!NOTE] +> Units used in this doc: all IDs are GUIDs; audiences are URI strings; role/scope values are string claims. + +## Setup + +### 1) Register APIM protected API app + +**Rule: APIM must validate tokens issued for the APIM API audience, not the ACA audience.** + +```text +Name: SimpleL7Proxy-APIM-API +Application ID URI: api:// +App role: API.Caller (Allowed member types: Applications) +``` + +> [!WARNING] +> If `Allowed member types` excludes `Applications`, app-to-app role assignment fails. + +### 2) Register ACA protected API app + +**Rule: ACA must expose its own audience and scope for inbound client tokens.** + +```text +Name: SimpleL7Proxy-ACA-API +Application ID URI: api:// +Scope: api.access +``` + +> [!TIP] +> Keep this audience distinct from APIM to avoid token confusion between hops. + +### 3) Register client app + +**Rule: the client app needs permission to ACA scope and must be allowed by ACA auth policy.** + +```text +Name: SimpleL7Proxy-Client +API permission: ACA API -> Delegated -> api.access +Credential: client secret (or cert) +``` + +> [!NOTE] +> For service-to-service calls, use client credentials and validate `roles` where applicable. + +### 4) Assign app roles with PowerShell (Graph) + +**Rule: use Graph PowerShell for app role assignments when managed identities do not appear in portal options.** + +```powershell +Connect-MgGraph -Scopes "Application.Read.All AppRoleAssignment.ReadWrite.All" +Select-MgProfile -Name "v1.0" +Get-MgContext +``` + +> [!TIP] +> If `Connect-MgGraph` fails on permissions, sign in with an Entra admin account and consent the requested scopes. + +#### 4a) Assign ACA managed identity -> APIM API role (`API.Caller`) + +```powershell +$acaSpId = "" +$apimResourceSpId = "" +$apimRoleId = "" +New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $acaSpId -PrincipalId $acaSpId -ResourceId $apimResourceSpId -AppRoleId $apimRoleId +``` + +> [!WARNING] +> Use object IDs, not app IDs, for `ServicePrincipalId`, `PrincipalId`, and `ResourceId`. + +#### 4b) Assign client service principal -> ACA API role (`API.Caller`) + +```powershell +$clientSpId = "" +$acaResourceSpId = "" +$acaRoleId = "" +New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $clientSpId -PrincipalId $clientSpId -ResourceId $acaResourceSpId -AppRoleId $acaRoleId +``` + +> [!NOTE] +> If you use delegated-only access to ACA (`api.access`), keep this step optional; for app-role based enforcement, keep it required. + +### 5) Configure ACA auth and APIM JWT validation + +**Rule: ACA validates client token audience; APIM validates ACA managed identity token audience and role.** + +```text +ACA allowed audience: api:// +APIM validate-jwt audience: api:// +APIM required claim: roles contains API.Caller +``` + +> [!WARNING] +> Passing ACA audience to APIM `validate-jwt` is a common misconfiguration and causes authorization failures. + +## Full flow + +```mermaid +flowchart LR + A[Client App Registration\n(SimpleL7Proxy-Client)] -->|Token aud=api://ACA_APP_ID| B[ACA Ingress + Easy Auth] + B -->|Validates ACA audience| C[SimpleL7Proxy in ACA] + C -->|Managed Identity token\naud=api://APIM_APP_ID/.default| D[APIM] + D -->|validate-jwt: audience + role API.Caller| E[Backend routing/policy] + + F[ACA API App Registration\n(SimpleL7Proxy-ACA-API)] -. defines audience/scope .-> B + G[APIM API App Registration\n(SimpleL7Proxy-APIM-API)] -. defines API.Caller role .-> D +``` + +## Worked example + +| Step | Example value | Result | +| :--- | :--- | :--- | +| Create APIM API app | `appId = 11111111-1111-1111-1111-111111111111` | APIM audience becomes `api://11111111-1111-1111-1111-111111111111` | +| Create ACA API app | `appId = 22222222-2222-2222-2222-222222222222` | ACA audience becomes `api://22222222-2222-2222-2222-222222222222` | +| Create client app | `appId = 33333333-3333-3333-3333-333333333333` | Client can request token for ACA audience | +| Assign ACA MI role on APIM API | `New-MgServicePrincipalAppRoleAssignment ...` | APIM accepts ACA token with `roles: API.Caller` | +| Request token in ACA for APIM | `resource = api://111.../.default` | ACA -> APIM call authorized | + +## Verify + +- [ ] APIM API app registration exists with app role `API.Caller`. +- [ ] ACA API app registration exists with scope `api.access` and identifier URI. +- [ ] Client app registration has permission to call ACA API. +- [ ] ACA managed identity is assigned to APIM API app role. +- [ ] ACA auth is enabled and configured with ACA audience. +- [ ] APIM `validate-jwt` checks APIM audience and `roles=API.Caller`. +- [ ] Client can call ACA with token for ACA audience. +- [ ] ACA can call APIM with managed identity token for APIM audience. + +## Related docs + +- [scripts/README.md](../scripts/README.md) +- [scripts/ca2apimSetup.sh](../scripts/ca2apimSetup.sh) +- [scripts/console2caSetup.sh](../scripts/console2caSetup.sh) +- [scripts/enableContainerAppAuth.sh](../scripts/enableContainerAppAuth.sh) +- [APIM-Policy/readme.md](../APIM-Policy/readme.md) From cb4273772adaa3d4d9139696a56e6035726de68d Mon Sep 17 00:00:00 2001 From: MarvelintheCloud Date: Thu, 21 May 2026 10:34:14 -0500 Subject: [PATCH 2/6] Fix Mermaid syntax in OAuth POC full-flow diagram, added jwt validation code, and added configuration steps for oauth in APIM interface --- docs/POC-Security-OAuth-Configuration.md | 82 +++++++++++++++++++----- 1 file changed, 67 insertions(+), 15 deletions(-) diff --git a/docs/POC-Security-OAuth-Configuration.md b/docs/POC-Security-OAuth-Configuration.md index 42cddc9..4406357 100644 --- a/docs/POC-Security-OAuth-Configuration.md +++ b/docs/POC-Security-OAuth-Configuration.md @@ -78,9 +78,9 @@ Credential: client secret (or cert) **Rule: use Graph PowerShell for app role assignments when managed identities do not appear in portal options.** ```powershell -Connect-MgGraph -Scopes "Application.Read.All AppRoleAssignment.ReadWrite.All" -Select-MgProfile -Name "v1.0" -Get-MgContext +Install-Module Microsoft.Graph.Applications -Scope CurrentUser -Repository PSGallery -Force +Import-Module Microsoft.Graph.Applications +Connect-MgGraph -TenantId "" -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All" ``` > [!TIP] @@ -89,14 +89,14 @@ Get-MgContext #### 4a) Assign ACA managed identity -> APIM API role (`API.Caller`) ```powershell -$acaSpId = "" +$acaSpId = "" $apimResourceSpId = "" $apimRoleId = "" New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $acaSpId -PrincipalId $acaSpId -ResourceId $apimResourceSpId -AppRoleId $apimRoleId ``` > [!WARNING] -> Use object IDs, not app IDs, for `ServicePrincipalId`, `PrincipalId`, and `ResourceId`. +> Use object IDs, not app IDs, for `ServicePrincipalId`, `PrincipalId`, and `ResourceId`. The managed identity object ID can be found on the ACA resource itself. For, the APIM Service Principal, you must use the object ID found under Enterprise Apps in Entra, NOT under the corresponding App Registrations. #### 4b) Assign client service principal -> ACA API role (`API.Caller`) @@ -108,7 +108,7 @@ New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $clientSpId -Princip ``` > [!NOTE] -> If you use delegated-only access to ACA (`api.access`), keep this step optional; for app-role based enforcement, keep it required. +> If you use delegated-only access to ACA (`api.access`), keep this step optional; for app-role based enforcement, keep it required. For, the Service Principal object IDs, you must use the object ID found under Enterprise Apps in Entra, NOT under the corresponding App Registrations. ### 5) Configure ACA auth and APIM JWT validation @@ -123,26 +123,78 @@ APIM required claim: roles contains API.Caller > [!WARNING] > Passing ACA audience to APIM `validate-jwt` is a common misconfiguration and causes authorization failures. +#### 5a) APIM inbound `validate-jwt` policy (example) + +**Rule: APIM must validate issuer + audience + role on the token ACA presents to APIM.** + +```xml + + + + + + api:// + + + https://login.microsoftonline.com//v2.0 + + + + API.Caller + + + + +``` + +> [!TIP] +> If your API uses delegated user tokens instead of app roles, validate `scp` instead of `roles`. + +#### 5b) Configure OAuth 2.0 in APIM interface (portal) + +**Rule: configure an APIM OAuth 2.0 authorization server for interactive auth/testing; keep `validate-jwt` as the enforcement control on APIs.** + +1. In Azure portal, open your APIM instance. +2. Go to Security -> OAuth 2.0 + OpenID Connect -> Add OAuth 2.0 server. +3. Set these fields: + - Display name: `EntraOAuth` (or your standard name) + - Grant types: `Authorization code` (and `Client credentials` if needed) + - Client ID: `` + - Client secret: `` + - Authorization endpoint URL: `https://login.microsoftonline.com//oauth2/v2.0/authorize` + - Token endpoint URL: `https://login.microsoftonline.com//oauth2/v2.0/token` + - Default scope: `api:///api.access` (or your API scope) +4. Save the OAuth 2.0 server. +5. Open your API in APIM -> Settings and attach this OAuth 2.0 server under Security if you want Developer Portal Authorize support. +6. Open your API -> Design -> Inbound processing and ensure the `validate-jwt` policy above is present. + +> [!NOTE] +> APIM OAuth server configuration enables the Authorize experience; token acceptance is still controlled by the API policy (`validate-jwt`). + ## Full flow ```mermaid flowchart LR - A[Client App Registration\n(SimpleL7Proxy-Client)] -->|Token aud=api://ACA_APP_ID| B[ACA Ingress + Easy Auth] - B -->|Validates ACA audience| C[SimpleL7Proxy in ACA] - C -->|Managed Identity token\naud=api://APIM_APP_ID/.default| D[APIM] - D -->|validate-jwt: audience + role API.Caller| E[Backend routing/policy] + A["Client App Registration
SimpleL7Proxy-Client"] -->|"Token aud=api://ACA_APP_ID"| B["ACA Ingress + Easy Auth"] + B -->|"Validates ACA audience"| C["SimpleL7Proxy in ACA"] + C -->|"Managed identity token
aud=api://APIM_APP_ID/.default"| D["APIM"] + D -->|"validate-jwt: audience + role API.Caller"| E["Backend routing/policy"] - F[ACA API App Registration\n(SimpleL7Proxy-ACA-API)] -. defines audience/scope .-> B - G[APIM API App Registration\n(SimpleL7Proxy-APIM-API)] -. defines API.Caller role .-> D + F["ACA API App Registration
SimpleL7Proxy-ACA-API"] -. "Defines audience and scope" .-> B + G["APIM API App Registration
SimpleL7Proxy-APIM-API"] -. "Defines API.Caller role" .-> D ``` ## Worked example | Step | Example value | Result | | :--- | :--- | :--- | -| Create APIM API app | `appId = 11111111-1111-1111-1111-111111111111` | APIM audience becomes `api://11111111-1111-1111-1111-111111111111` | -| Create ACA API app | `appId = 22222222-2222-2222-2222-222222222222` | ACA audience becomes `api://22222222-2222-2222-2222-222222222222` | -| Create client app | `appId = 33333333-3333-3333-3333-333333333333` | Client can request token for ACA audience | +| Create APIM API app registration | `appId = 11111111-1111-1111-1111-111111111111` | APIM audience becomes `api://11111111-1111-1111-1111-111111111111` | +| Create ACA API app registration | `appId = 22222222-2222-2222-2222-222222222222` | ACA audience becomes `api://22222222-2222-2222-2222-222222222222` | +| Create client app registration | `appId = 33333333-3333-3333-3333-333333333333` | Client can request token for ACA audience | | Assign ACA MI role on APIM API | `New-MgServicePrincipalAppRoleAssignment ...` | APIM accepts ACA token with `roles: API.Caller` | | Request token in ACA for APIM | `resource = api://111.../.default` | ACA -> APIM call authorized | From bd708e8ade6de6a3666fb4a11646439131f823dd Mon Sep 17 00:00:00 2001 From: MarvelintheCloud Date: Thu, 21 May 2026 10:53:01 -0500 Subject: [PATCH 3/6] Add APIM policy test steps for OAuth configuration POC --- docs/POC-Security-OAuth-Configuration.md | 99 +++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/docs/POC-Security-OAuth-Configuration.md b/docs/POC-Security-OAuth-Configuration.md index 4406357..b3b8575 100644 --- a/docs/POC-Security-OAuth-Configuration.md +++ b/docs/POC-Security-OAuth-Configuration.md @@ -25,6 +25,7 @@ | ACA scope value | `api.access` | scope value | ACA API app registration | token issuance | | APIM audience | `api://` | URI | APIM `validate-jwt` policy | policy save | | ACA audience | `api://` | URI | ACA auth config | config save | +| Client secret requirement by app | APIM API app: `No`; ACA API app: `Yes` (when used by ACA auth config); Client app: `Yes` (client credentials) | flag | Entra App registrations | immediate | | Client -> ACA token resource | `api://` | URI | token request | per request | | ACA -> APIM token resource | `api:///.default` | URI | managed identity token request | per request | | Graph module permission | `AppRoleAssignment.ReadWrite.All` | Graph scope | `Connect-MgGraph` | login session | @@ -73,6 +74,24 @@ Credential: client secret (or cert) > [!NOTE] > For service-to-service calls, use client credentials and validate `roles` where applicable. +### 3a) Client secret requirements by app registration + +**Rule: only apps that actively request tokens as confidential clients need a client secret.** + +1. APIM protected API app (`SimpleL7Proxy-APIM-API`): no client secret required for this POC. +2. ACA protected API app (`SimpleL7Proxy-ACA-API`): create a client secret if you configure ACA Easy Auth with Entra app credentials (`-c` and `-s` values in `enableContainerAppAuth.sh`). +3. Client app (`SimpleL7Proxy-Client`): create a client secret (or certificate) when using client credentials flow. + +Portal steps to create a secret: + +1. Entra ID -> App registrations -> select the app. +2. Go to Certificates & secrets -> New client secret. +3. Add description + expiry, then create. +4. Copy the secret Value immediately and store it securely. + +> [!WARNING] +> Secret values are shown only once. If lost, create a new secret and update ACA/APIM config that depends on it. + ### 4) Assign app roles with PowerShell (Graph) **Rule: use Graph PowerShell for app role assignments when managed identities do not appear in portal options.** @@ -163,8 +182,8 @@ APIM required claim: roles contains API.Caller 3. Set these fields: - Display name: `EntraOAuth` (or your standard name) - Grant types: `Authorization code` (and `Client credentials` if needed) - - Client ID: `` - - Client secret: `` + - Client ID: `` (typically the client app registration) + - Client secret: `` - Authorization endpoint URL: `https://login.microsoftonline.com//oauth2/v2.0/authorize` - Token endpoint URL: `https://login.microsoftonline.com//oauth2/v2.0/token` - Default scope: `api:///api.access` (or your API scope) @@ -175,6 +194,82 @@ APIM required claim: roles contains API.Caller > [!NOTE] > APIM OAuth server configuration enables the Authorize experience; token acceptance is still controlled by the API policy (`validate-jwt`). +### 6) Test APIM policy after configuration + +**Rule: validate both positive and negative paths to confirm `validate-jwt` is enforcing audience and role correctly.** + +Set your test variables first: + +```bash +APIM_BASE="https://.azure-api.net/" +APIM_SUB_KEY="" +TENANT_ID="" +APIM_APP_ID="" +``` + +#### 6a) Positive test: ACA managed identity (or equivalent caller) succeeds + +```bash +# This token should be requested for APIM audience: api:///.default +TOKEN="" + +curl -i "$APIM_BASE/health" \ + -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" \ + -H "Authorization: Bearer $TOKEN" +``` + +Expected result: + +- `200` (or your API's expected success code) +- No `Unauthorized. Missing or invalid token.` message + +#### 6b) Negative test: no token should fail + +```bash +curl -i "$APIM_BASE/health" \ + -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" +``` + +Expected result: + +- `401 Unauthorized` +- Error from `validate-jwt` policy + +#### 6c) Negative test: wrong audience should fail + +```bash +# Use a token for ACA audience instead of APIM audience. +BAD_TOKEN="" + +curl -i "$APIM_BASE/health" \ + -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" \ + -H "Authorization: Bearer $BAD_TOKEN" +``` + +Expected result: + +- `401 Unauthorized` +- Audience validation failure + +#### 6d) Negative test: missing role should fail + +```bash +# Use a token that has APIM audience but lacks roles: API.Caller. +NO_ROLE_TOKEN="" + +curl -i "$APIM_BASE/health" \ + -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" \ + -H "Authorization: Bearer $NO_ROLE_TOKEN" +``` + +Expected result: + +- `401 Unauthorized` +- Required claim (`roles=API.Caller`) validation failure + +> [!TIP] +> For fast diagnosis, temporarily project token claims in APIM trace and verify `aud`, `iss`, and `roles` match your `validate-jwt` policy. + ## Full flow ```mermaid From d3a380a3eee89a6050444a05f4a95e4cd7c7ec2f Mon Sep 17 00:00:00 2001 From: MarvelintheCloud Date: Thu, 21 May 2026 11:02:19 -0500 Subject: [PATCH 4/6] Added instructions for creating app registrations --- docs/POC-Security-OAuth-Configuration.md | 62 ++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/docs/POC-Security-OAuth-Configuration.md b/docs/POC-Security-OAuth-Configuration.md index b3b8575..e19d8a0 100644 --- a/docs/POC-Security-OAuth-Configuration.md +++ b/docs/POC-Security-OAuth-Configuration.md @@ -35,7 +35,7 @@ ## Setup -### 1) Register APIM protected API app +### 1) Create an App Registration for APIM in Entra **Rule: APIM must validate tokens issued for the APIM API audience, not the ACA audience.** @@ -45,10 +45,27 @@ Application ID URI: api:// App role: API.Caller (Allowed member types: Applications) ``` +Portal steps (repo-aligned): + +1. Go to Entra ID -> App registrations -> New registration. +2. Name it `SimpleL7Proxy-APIM-API` (or your environment naming standard), then create. +3. Open Expose an API -> Set Application ID URI -> `api://`. +4. Open App roles -> Create app role with: + - Display name: `Caller` + - Allowed member types: `Users/Groups` and `Applications` + - Value: `API.Caller` + - Description: `Caller` + - Enable app role: `Yes` +5. Open Enterprise applications -> find this app's service principal -> set assignment required to `Yes` (repo script equivalent: `appRoleAssignmentRequired=true`). +6. Capture and save these IDs for later steps: + - App (client) ID (`APIM_APP_ID`) + - Service principal object ID (`APIM_API_SERVICE_PRINCIPAL_OBJECT_ID`) + - App role ID for `API.Caller` (`APIM_API_CALLER_ROLE_ID`) + > [!WARNING] > If `Allowed member types` excludes `Applications`, app-to-app role assignment fails. -### 2) Register ACA protected API app +### 2) Create an App Registration for ACA in Entra **Rule: ACA must expose its own audience and scope for inbound client tokens.** @@ -58,10 +75,32 @@ Application ID URI: api:// Scope: api.access ``` +Portal steps (repo-aligned): + +1. Go to Entra ID -> App registrations -> New registration. +2. Name it `SimpleL7Proxy-ACA-API`, then create. +3. Open Expose an API -> Set Application ID URI -> `api://`. +4. In Expose an API -> Add a scope with: + - Scope name/value: `api.access` + - Who can consent: `Admins only` (repo script sets scope type `Admin`) + - Admin consent display name: `Admin Access` + - Admin consent description: `Access the API` + - State: `Enabled` +5. Open App roles -> Create app role with: + - Display name: `Caller` + - Allowed member types: `Users/Groups` and `Applications` + - Value: `API.Caller` + - Enable app role: `Yes` +6. Open Enterprise applications -> find this app's service principal -> set assignment required to `Yes`. +7. Capture and save these IDs for later steps: + - App (client) ID (`ACA_APP_ID`) + - Service principal object ID (`ACA_API_SERVICE_PRINCIPAL_OBJECT_ID`) + - App role ID for `API.Caller` (`ACA_API_CALLER_ROLE_ID`) + > [!TIP] > Keep this audience distinct from APIM to avoid token confusion between hops. -### 3) Register client app +### 3) Create an App Registration for client app in Entra **Rule: the client app needs permission to ACA scope and must be allowed by ACA auth policy.** @@ -71,6 +110,23 @@ API permission: ACA API -> Delegated -> api.access Credential: client secret (or cert) ``` +Portal steps (repo-aligned): + +1. Go to Entra ID -> App registrations -> New registration. +2. Name it `SimpleL7Proxy-Client`, then create. +3. Open API permissions -> Add a permission -> My APIs -> select `SimpleL7Proxy-ACA-API`. +4. Add delegated permission `api.access`. +5. If required by tenant policy, select Grant admin consent. +6. Open Certificates & secrets -> create a client secret (or configure a certificate). +7. Ensure a service principal exists for this app in Enterprise applications (repo script equivalent creates one explicitly). +8. Capture and save: + - App (client) ID (`CLIENT_APP_ID`) + - Service principal object ID (`CLIENT_SERVICE_PRINCIPAL_OBJECT_ID`) + - Client secret value (`CLIENT_SECRET`) + +> [!NOTE] +> The repo scripts use this client identity as the caller to ACA and then assign app roles to its service principal as needed. + > [!NOTE] > For service-to-service calls, use client credentials and validate `roles` where applicable. From 4c1b2b1e4aa62d7afe396144054ac468d4b29fa9 Mon Sep 17 00:00:00 2001 From: MarvelintheCloud Date: Thu, 21 May 2026 20:31:41 -0500 Subject: [PATCH 5/6] Split security OAuth POC into ACA and APIM focused docs --- docs/POC-ACA-Proxy-Security-Authorization.md | 201 ++++++++++ docs/POC-APIM-Security-Authorization.md | 207 ++++++++++ docs/POC-Security-OAuth-Configuration.md | 378 +------------------ 3 files changed, 425 insertions(+), 361 deletions(-) create mode 100644 docs/POC-ACA-Proxy-Security-Authorization.md create mode 100644 docs/POC-APIM-Security-Authorization.md diff --git a/docs/POC-ACA-Proxy-Security-Authorization.md b/docs/POC-ACA-Proxy-Security-Authorization.md new file mode 100644 index 0000000..07920a8 --- /dev/null +++ b/docs/POC-ACA-Proxy-Security-Authorization.md @@ -0,0 +1,201 @@ +# POC: ACA Proxy Security and Authorization + +**Purpose:** Validate inbound OAuth 2.0 authentication and authorization for the SimpleL7Proxy Container App (ACA), including Entra app registration setup, ACA auth configuration, and caller validation. + +## TL;DR (< 5 minutes) + +1. **Most important rule: tokens sent to ACA must have `aud = api://`.** +2. Create two Entra apps for this hop: ACA API app and client caller app. +3. Enable ACA authentication and test both success and failure paths. + +## What you will observe + +- A token minted for `api://` is accepted by ACA. +- Requests without a token or with the wrong audience are rejected. +- The client identity can be restricted through allowed applications and app role assignment. + +## Reference + +| Setting | Value in this POC | Unit | Set in | Takes effect | +| :--- | :--- | :--- | :--- | :--- | +| ACA API app registration | `SimpleL7Proxy-ACA-API` | name | Entra App registrations | immediate | +| Client app registration | `SimpleL7Proxy-Client` | name | Entra App registrations | immediate | +| ACA audience | `api://` | URI | ACA auth config | config save | +| ACA scope | `api.access` | scope value | ACA API app | token issuance | +| App role value | `API.Caller` | claim value | ACA API app | token issuance | +| Client secret requirement | ACA API app: `Yes` when used in ACA auth config; Client app: `Yes` for client credentials flow | flag | Entra App registrations | immediate | + +> [!NOTE] +> Units used in this doc: IDs are GUIDs, audience values are URI strings, and roles/scopes are string claims. + +## Setup + +### 1) Create ACA API app registration in Entra + +**Rule: the ACA proxy must expose its own audience and scope for inbound client tokens.** + +```text +Name: SimpleL7Proxy-ACA-API +Application ID URI: api:// +Scope: api.access +App role: API.Caller +``` + +Portal steps (repo-aligned): + +1. Entra ID -> App registrations -> New registration. +2. Name: `SimpleL7Proxy-ACA-API`. +3. Expose an API -> Set Application ID URI -> `api://`. +4. Expose an API -> Add scope: + - Scope value: `api.access` + - Who can consent: `Admins only` + - Admin consent display name: `Admin Access` + - Admin consent description: `Access the API` + - State: `Enabled` +5. App roles -> Create app role: + - Display name: `Caller` + - Allowed member types: `Users/Groups` and `Applications` + - Value: `API.Caller` + - Enable app role: `Yes` +6. Enterprise applications -> corresponding service principal -> set assignment required to `Yes`. +7. Save IDs: + - `ACA_APP_ID` + - `ACA_API_SERVICE_PRINCIPAL_OBJECT_ID` + - `ACA_API_CALLER_ROLE_ID` + +### 2) Create client app registration in Entra + +**Rule: the caller must be able to request a token for ACA scope and present it to ACA.** + +```text +Name: SimpleL7Proxy-Client +Permission: ACA API delegated permission api.access +Credential: client secret (or certificate) +``` + +Portal steps: + +1. Entra ID -> App registrations -> New registration. +2. Name: `SimpleL7Proxy-Client`. +3. API permissions -> Add permission -> My APIs -> `SimpleL7Proxy-ACA-API`. +4. Add delegated permission `api.access`. +5. Grant admin consent if required by tenant policy. +6. Certificates and secrets -> New client secret. +7. Save IDs/secrets: + - `CLIENT_APP_ID` + - `CLIENT_SERVICE_PRINCIPAL_OBJECT_ID` + - `CLIENT_SECRET` + +### 3) Configure ACA authentication + +**Rule: ACA auth must validate the ACA audience and only allow authorized caller applications.** + +Script-aligned configuration using repo flow: + +```bash +./scripts/enableContainerAppAuth.sh \ + -g \ + -n \ + -t \ + -c \ + -s \ + -a +``` + +Equivalent portal checks: + +1. Container Apps -> your app -> Authentication. +2. Identity provider: Microsoft. +3. Tenant: ``. +4. Client ID: ``. +5. Client secret: ACA app secret. +6. Allowed token audiences includes `api://`. +7. Allowed applications includes ``. +8. Unauthenticated requests set to `401`/reject. + +> [!WARNING] +> Use tenant ID for `-t`; do not use a service principal object ID in this field. + +### 4) Optional app-role assignment for client service principal + +**Rule: if you enforce role-based app auth, assign `API.Caller` to the client service principal on ACA API.** + +```powershell +Connect-MgGraph -TenantId "" -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All" +$clientSpId = "" +$acaResourceSpId = "" +$acaRoleId = "" +New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $clientSpId -PrincipalId $clientSpId -ResourceId $acaResourceSpId -AppRoleId $acaRoleId +``` + +## Full flow + +```mermaid +flowchart LR + A["Client App
SimpleL7Proxy-Client"] -->|"Bearer token aud=api://ACA_APP_ID"| B["ACA Ingress + Easy Auth"] + B -->|"Validate audience + app constraints"| C["SimpleL7Proxy in ACA"] + D["ACA API App
SimpleL7Proxy-ACA-API"] -. "Defines audience, scope, and optional role" .-> B +``` + +## Worked example + +| Step | Example value | Result | +| :--- | :--- | :--- | +| Create ACA API app | `appId = 22222222-2222-2222-2222-222222222222` | ACA audience is `api://22222222-2222-2222-2222-222222222222` | +| Create client app | `appId = 33333333-3333-3333-3333-333333333333` | Client can request ACA token | +| Enable ACA auth | Allowed audience set to ACA URI | ACA validates incoming bearer tokens | +| Send valid token | `aud` matches ACA URI | Request succeeds | +| Send token with wrong audience | `aud` is APIM URI | Request fails with `401` | + +## Test ACA authorization + +**Rule: run one success test and two failure tests to validate auth controls.** + +Set variables: + +```bash +ACA_FQDN="https://" +ACA_RESOURCE="api://" +``` + +Positive test: + +```bash +TOKEN="$(az account get-access-token --resource "$ACA_RESOURCE" --query accessToken -o tsv)" +curl -i "$ACA_FQDN/health" -H "Authorization: Bearer $TOKEN" +``` + +Expected: success response from ACA. + +Negative test (no token): + +```bash +curl -i "$ACA_FQDN/health" +``` + +Expected: `401 Unauthorized`. + +Negative test (wrong audience token): + +```bash +BAD_TOKEN="$(az account get-access-token --resource api:// --query accessToken -o tsv)" +curl -i "$ACA_FQDN/health" -H "Authorization: Bearer $BAD_TOKEN" +``` + +Expected: `401 Unauthorized` due to audience mismatch. + +## Verify + +- [ ] ACA API app exists with `api://` identifier URI. +- [ ] ACA API app exposes scope `api.access`. +- [ ] Client app has delegated permission to `api.access`. +- [ ] ACA auth is enabled and allowed audience includes ACA URI. +- [ ] Allowed applications includes client app ID. +- [ ] Valid ACA token succeeds; missing/wrong-audience token fails. + +## Related docs + +- [scripts/README.md](../scripts/README.md) +- [scripts/console2caSetup.sh](../scripts/console2caSetup.sh) +- [scripts/enableContainerAppAuth.sh](../scripts/enableContainerAppAuth.sh) +- [POC-APIM-Security-Authorization.md](POC-APIM-Security-Authorization.md) diff --git a/docs/POC-APIM-Security-Authorization.md b/docs/POC-APIM-Security-Authorization.md new file mode 100644 index 0000000..2afc8ff --- /dev/null +++ b/docs/POC-APIM-Security-Authorization.md @@ -0,0 +1,207 @@ +# POC: APIM Security and Authorization + +**Purpose:** Validate OAuth 2.0 authentication and authorization at APIM for calls coming from ACA, including Entra app registration, app-role assignment, APIM OAuth interface setup, and `validate-jwt` enforcement. + +## TL;DR (< 5 minutes) + +1. **Most important rule: APIM must only accept tokens with `aud = api://` and `roles = API.Caller`.** +2. Create an APIM API app registration and assign `API.Caller` to ACA managed identity. +3. Configure APIM `validate-jwt` and test both success and failure paths. + +## What you will observe + +- APIM accepts calls with valid bearer tokens minted for APIM audience and role. +- APIM rejects calls with no token, wrong audience, or missing role. +- OAuth server config in APIM enables interactive authorizing, while policy still enforces acceptance. + +## Reference + +| Setting | Value in this POC | Unit | Set in | Takes effect | +| :--- | :--- | :--- | :--- | :--- | +| APIM API app registration | `SimpleL7Proxy-APIM-API` | name | Entra App registrations | immediate | +| App role value | `API.Caller` | claim value | APIM API app | token issuance | +| APIM audience | `api://` | URI | APIM `validate-jwt` policy | policy save | +| ACA caller identity | ACA managed identity service principal | principal | ACA + Entra | immediate | +| OAuth server client secret | secret from client app used in APIM OAuth server UI | secret | Entra + APIM | save | + +> [!NOTE] +> Units used in this doc: IDs are GUIDs and audience values are URI strings. + +## Setup + +### 1) Create APIM API app registration in Entra + +**Rule: APIM policy audience must match this app's identifier URI.** + +```text +Name: SimpleL7Proxy-APIM-API +Application ID URI: api:// +App role: API.Caller +``` + +Portal steps (repo-aligned): + +1. Entra ID -> App registrations -> New registration. +2. Name: `SimpleL7Proxy-APIM-API`. +3. Expose an API -> Set Application ID URI -> `api://`. +4. App roles -> Create app role: + - Display name: `Caller` + - Allowed member types: `Users/Groups` and `Applications` + - Value: `API.Caller` + - Enable app role: `Yes` +5. Enterprise applications -> corresponding service principal -> set assignment required to `Yes`. +6. Save IDs: + - `APIM_APP_ID` + - `APIM_API_SERVICE_PRINCIPAL_OBJECT_ID` + - `APIM_API_CALLER_ROLE_ID` + +### 2) Assign ACA managed identity to APIM role + +**Rule: ACA managed identity must have `API.Caller` role on APIM API enterprise app.** + +```powershell +Connect-MgGraph -TenantId "" -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All" +$acaSpId = "" +$apimResourceSpId = "" +$apimRoleId = "" +New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $acaSpId -PrincipalId $acaSpId -ResourceId $apimResourceSpId -AppRoleId $apimRoleId +``` + +> [!WARNING] +> Use service principal object IDs from Enterprise applications, not app registration app IDs. + +### 3) Configure APIM inbound `validate-jwt` policy + +**Rule: APIM must validate issuer, audience, and role before backend routing.** + +```xml + + + + + + api:// + + + https://login.microsoftonline.com//v2.0 + + + + API.Caller + + + + +``` + +> [!TIP] +> If your APIM endpoint accepts delegated user tokens, validate `scp` instead of `roles`. + +### 4) Configure OAuth 2.0 in APIM interface + +**Rule: OAuth server settings support authorize/testing UX, while `validate-jwt` policy remains the true gate.** + +1. Azure portal -> APIM instance. +2. Security -> OAuth 2.0 + OpenID Connect -> Add OAuth 2.0 server. +3. Configure: + - Display name: `EntraOAuth` + - Grant types: `Authorization code` and optionally `Client credentials` + - Client ID: `` + - Client secret: `` + - Authorization endpoint: `https://login.microsoftonline.com//oauth2/v2.0/authorize` + - Token endpoint: `https://login.microsoftonline.com//oauth2/v2.0/token` + - Default scope: `api:///.default` (or your API scope) +4. Save. +5. API -> Settings -> attach OAuth server if Developer Portal Authorize is needed. +6. API -> Design -> verify `validate-jwt` is present in inbound policy. + +## Full flow + +```mermaid +flowchart LR + A["ACA Managed Identity"] -->|"Bearer token aud=api://APIM_APP_ID/.default"| B["APIM validate-jwt"] + B -->|"Checks issuer + audience + roles=API.Caller"| C["APIM backend routing"] + D["APIM API App
SimpleL7Proxy-APIM-API"] -. "Defines audience and API.Caller role" .-> B +``` + +## Worked example + +| Step | Example value | Result | +| :--- | :--- | :--- | +| Create APIM API app | `appId = 11111111-1111-1111-1111-111111111111` | APIM audience is `api://11111111-1111-1111-1111-111111111111` | +| Assign ACA managed identity role | `API.Caller` granted | ACA token can satisfy APIM role check | +| Apply validate-jwt policy | audience + role checks active | Unauthorized tokens are blocked | +| Send valid APIM token | `aud` and `roles` match | Request succeeds | +| Send ACA audience token | `aud` mismatch | Request fails with `401` | + +## Test APIM authorization policy + +**Rule: run one positive and three negative tests to confirm policy enforcement.** + +Set variables: + +```bash +APIM_BASE="https://.azure-api.net/" +APIM_SUB_KEY="" +``` + +Positive test: + +```bash +TOKEN="" +curl -i "$APIM_BASE/health" \ + -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" \ + -H "Authorization: Bearer $TOKEN" +``` + +Expected: success response. + +Negative test (no token): + +```bash +curl -i "$APIM_BASE/health" -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" +``` + +Expected: `401 Unauthorized`. + +Negative test (wrong audience): + +```bash +BAD_TOKEN="" +curl -i "$APIM_BASE/health" \ + -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" \ + -H "Authorization: Bearer $BAD_TOKEN" +``` + +Expected: `401 Unauthorized` due to audience mismatch. + +Negative test (missing role): + +```bash +NO_ROLE_TOKEN="" +curl -i "$APIM_BASE/health" \ + -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" \ + -H "Authorization: Bearer $NO_ROLE_TOKEN" +``` + +Expected: `401 Unauthorized` due to missing required claim. + +## Verify + +- [ ] APIM API app registration exists with identifier URI `api://`. +- [ ] App role `API.Caller` exists and allows `Applications`. +- [ ] ACA managed identity is assigned `API.Caller` on APIM API enterprise app. +- [ ] APIM inbound policy validates issuer, audience, and role. +- [ ] Valid APIM token succeeds. +- [ ] Missing token, wrong audience, and missing role all fail with `401`. + +## Related docs + +- [scripts/README.md](../scripts/README.md) +- [scripts/ca2apimSetup.sh](../scripts/ca2apimSetup.sh) +- [APIM-Policy/readme.md](../APIM-Policy/readme.md) +- [POC-ACA-Proxy-Security-Authorization.md](POC-ACA-Proxy-Security-Authorization.md) diff --git a/docs/POC-Security-OAuth-Configuration.md b/docs/POC-Security-OAuth-Configuration.md index e19d8a0..3250103 100644 --- a/docs/POC-Security-OAuth-Configuration.md +++ b/docs/POC-Security-OAuth-Configuration.md @@ -1,369 +1,25 @@ -# POC: Security and OAuth 2.0 Configuration +# POC: Security and OAuth 2.0 Configuration (Split Index) -**Purpose:** Validate the repo-aligned OAuth 2.0 trust chain across client -> Container App -> APIM, including the three Entra app registrations and Graph-based app role assignments. +**Purpose:** This index replaces the previous combined security POC and points to two focused runbooks. -## TL;DR (< 5 minutes) +## TL;DR -1. **Most important rule: each hop must use its own token audience (`aud`) and role check; do not pass the same token end-to-end.** -2. Create three app registrations: APIM protected API, ACA protected API, and client caller app. -3. Assign app roles with Microsoft Graph PowerShell when portal UI cannot target managed identities. +1. **Most important rule: use the doc that matches the security boundary you are configuring.** +2. Use the ACA document for client -> ACA authentication and authorization. +3. Use the APIM document for ACA -> APIM token validation and policy enforcement. -## What you will observe +## Use these documents -- Client obtains a token for ACA (`aud = api://`) and can call ACA when ACA auth is enabled. -- ACA uses managed identity to obtain a separate token for APIM (`aud = api://`) and APIM accepts `roles = API.Caller`. -- App role assignment succeeds for identities not selectable in portal UI by using Graph PowerShell cmdlets. +- [POC-ACA-Proxy-Security-Authorization.md](POC-ACA-Proxy-Security-Authorization.md) + - Focus: securing and authorizing the ACA proxy ingress. + - Includes: ACA app registration, client app registration, ACA auth setup, and ACA validation tests. -## Reference +- [POC-APIM-Security-Authorization.md](POC-APIM-Security-Authorization.md) + - Focus: securing and authorizing APIM. + - Includes: APIM app registration, ACA managed identity role assignment, APIM OAuth interface setup, `validate-jwt` policy, and APIM policy tests. -| Setting | Value in this POC | Unit | Set in | Takes effect | -| :--- | :--- | :--- | :--- | :--- | -| APIM API app registration | `SimpleL7Proxy-APIM-API` | name | Entra App registrations | immediate | -| ACA API app registration | `SimpleL7Proxy-ACA-API` | name | Entra App registrations | immediate | -| Client app registration | `SimpleL7Proxy-Client` | name | Entra App registrations | immediate | -| App role value | `API.Caller` | claim value | APIM API + ACA API app registrations | token issuance | -| ACA scope value | `api.access` | scope value | ACA API app registration | token issuance | -| APIM audience | `api://` | URI | APIM `validate-jwt` policy | policy save | -| ACA audience | `api://` | URI | ACA auth config | config save | -| Client secret requirement by app | APIM API app: `No`; ACA API app: `Yes` (when used by ACA auth config); Client app: `Yes` (client credentials) | flag | Entra App registrations | immediate | -| Client -> ACA token resource | `api://` | URI | token request | per request | -| ACA -> APIM token resource | `api:///.default` | URI | managed identity token request | per request | -| Graph module permission | `AppRoleAssignment.ReadWrite.All` | Graph scope | `Connect-MgGraph` | login session | +## Why the split -> [!NOTE] -> Units used in this doc: all IDs are GUIDs; audiences are URI strings; role/scope values are string claims. - -## Setup - -### 1) Create an App Registration for APIM in Entra - -**Rule: APIM must validate tokens issued for the APIM API audience, not the ACA audience.** - -```text -Name: SimpleL7Proxy-APIM-API -Application ID URI: api:// -App role: API.Caller (Allowed member types: Applications) -``` - -Portal steps (repo-aligned): - -1. Go to Entra ID -> App registrations -> New registration. -2. Name it `SimpleL7Proxy-APIM-API` (or your environment naming standard), then create. -3. Open Expose an API -> Set Application ID URI -> `api://`. -4. Open App roles -> Create app role with: - - Display name: `Caller` - - Allowed member types: `Users/Groups` and `Applications` - - Value: `API.Caller` - - Description: `Caller` - - Enable app role: `Yes` -5. Open Enterprise applications -> find this app's service principal -> set assignment required to `Yes` (repo script equivalent: `appRoleAssignmentRequired=true`). -6. Capture and save these IDs for later steps: - - App (client) ID (`APIM_APP_ID`) - - Service principal object ID (`APIM_API_SERVICE_PRINCIPAL_OBJECT_ID`) - - App role ID for `API.Caller` (`APIM_API_CALLER_ROLE_ID`) - -> [!WARNING] -> If `Allowed member types` excludes `Applications`, app-to-app role assignment fails. - -### 2) Create an App Registration for ACA in Entra - -**Rule: ACA must expose its own audience and scope for inbound client tokens.** - -```text -Name: SimpleL7Proxy-ACA-API -Application ID URI: api:// -Scope: api.access -``` - -Portal steps (repo-aligned): - -1. Go to Entra ID -> App registrations -> New registration. -2. Name it `SimpleL7Proxy-ACA-API`, then create. -3. Open Expose an API -> Set Application ID URI -> `api://`. -4. In Expose an API -> Add a scope with: - - Scope name/value: `api.access` - - Who can consent: `Admins only` (repo script sets scope type `Admin`) - - Admin consent display name: `Admin Access` - - Admin consent description: `Access the API` - - State: `Enabled` -5. Open App roles -> Create app role with: - - Display name: `Caller` - - Allowed member types: `Users/Groups` and `Applications` - - Value: `API.Caller` - - Enable app role: `Yes` -6. Open Enterprise applications -> find this app's service principal -> set assignment required to `Yes`. -7. Capture and save these IDs for later steps: - - App (client) ID (`ACA_APP_ID`) - - Service principal object ID (`ACA_API_SERVICE_PRINCIPAL_OBJECT_ID`) - - App role ID for `API.Caller` (`ACA_API_CALLER_ROLE_ID`) - -> [!TIP] -> Keep this audience distinct from APIM to avoid token confusion between hops. - -### 3) Create an App Registration for client app in Entra - -**Rule: the client app needs permission to ACA scope and must be allowed by ACA auth policy.** - -```text -Name: SimpleL7Proxy-Client -API permission: ACA API -> Delegated -> api.access -Credential: client secret (or cert) -``` - -Portal steps (repo-aligned): - -1. Go to Entra ID -> App registrations -> New registration. -2. Name it `SimpleL7Proxy-Client`, then create. -3. Open API permissions -> Add a permission -> My APIs -> select `SimpleL7Proxy-ACA-API`. -4. Add delegated permission `api.access`. -5. If required by tenant policy, select Grant admin consent. -6. Open Certificates & secrets -> create a client secret (or configure a certificate). -7. Ensure a service principal exists for this app in Enterprise applications (repo script equivalent creates one explicitly). -8. Capture and save: - - App (client) ID (`CLIENT_APP_ID`) - - Service principal object ID (`CLIENT_SERVICE_PRINCIPAL_OBJECT_ID`) - - Client secret value (`CLIENT_SECRET`) - -> [!NOTE] -> The repo scripts use this client identity as the caller to ACA and then assign app roles to its service principal as needed. - -> [!NOTE] -> For service-to-service calls, use client credentials and validate `roles` where applicable. - -### 3a) Client secret requirements by app registration - -**Rule: only apps that actively request tokens as confidential clients need a client secret.** - -1. APIM protected API app (`SimpleL7Proxy-APIM-API`): no client secret required for this POC. -2. ACA protected API app (`SimpleL7Proxy-ACA-API`): create a client secret if you configure ACA Easy Auth with Entra app credentials (`-c` and `-s` values in `enableContainerAppAuth.sh`). -3. Client app (`SimpleL7Proxy-Client`): create a client secret (or certificate) when using client credentials flow. - -Portal steps to create a secret: - -1. Entra ID -> App registrations -> select the app. -2. Go to Certificates & secrets -> New client secret. -3. Add description + expiry, then create. -4. Copy the secret Value immediately and store it securely. - -> [!WARNING] -> Secret values are shown only once. If lost, create a new secret and update ACA/APIM config that depends on it. - -### 4) Assign app roles with PowerShell (Graph) - -**Rule: use Graph PowerShell for app role assignments when managed identities do not appear in portal options.** - -```powershell -Install-Module Microsoft.Graph.Applications -Scope CurrentUser -Repository PSGallery -Force -Import-Module Microsoft.Graph.Applications -Connect-MgGraph -TenantId "" -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All" -``` - -> [!TIP] -> If `Connect-MgGraph` fails on permissions, sign in with an Entra admin account and consent the requested scopes. - -#### 4a) Assign ACA managed identity -> APIM API role (`API.Caller`) - -```powershell -$acaSpId = "" -$apimResourceSpId = "" -$apimRoleId = "" -New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $acaSpId -PrincipalId $acaSpId -ResourceId $apimResourceSpId -AppRoleId $apimRoleId -``` - -> [!WARNING] -> Use object IDs, not app IDs, for `ServicePrincipalId`, `PrincipalId`, and `ResourceId`. The managed identity object ID can be found on the ACA resource itself. For, the APIM Service Principal, you must use the object ID found under Enterprise Apps in Entra, NOT under the corresponding App Registrations. - -#### 4b) Assign client service principal -> ACA API role (`API.Caller`) - -```powershell -$clientSpId = "" -$acaResourceSpId = "" -$acaRoleId = "" -New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $clientSpId -PrincipalId $clientSpId -ResourceId $acaResourceSpId -AppRoleId $acaRoleId -``` - -> [!NOTE] -> If you use delegated-only access to ACA (`api.access`), keep this step optional; for app-role based enforcement, keep it required. For, the Service Principal object IDs, you must use the object ID found under Enterprise Apps in Entra, NOT under the corresponding App Registrations. - -### 5) Configure ACA auth and APIM JWT validation - -**Rule: ACA validates client token audience; APIM validates ACA managed identity token audience and role.** - -```text -ACA allowed audience: api:// -APIM validate-jwt audience: api:// -APIM required claim: roles contains API.Caller -``` - -> [!WARNING] -> Passing ACA audience to APIM `validate-jwt` is a common misconfiguration and causes authorization failures. - -#### 5a) APIM inbound `validate-jwt` policy (example) - -**Rule: APIM must validate issuer + audience + role on the token ACA presents to APIM.** - -```xml - - - - - - api:// - - - https://login.microsoftonline.com//v2.0 - - - - API.Caller - - - - -``` - -> [!TIP] -> If your API uses delegated user tokens instead of app roles, validate `scp` instead of `roles`. - -#### 5b) Configure OAuth 2.0 in APIM interface (portal) - -**Rule: configure an APIM OAuth 2.0 authorization server for interactive auth/testing; keep `validate-jwt` as the enforcement control on APIs.** - -1. In Azure portal, open your APIM instance. -2. Go to Security -> OAuth 2.0 + OpenID Connect -> Add OAuth 2.0 server. -3. Set these fields: - - Display name: `EntraOAuth` (or your standard name) - - Grant types: `Authorization code` (and `Client credentials` if needed) - - Client ID: `` (typically the client app registration) - - Client secret: `` - - Authorization endpoint URL: `https://login.microsoftonline.com//oauth2/v2.0/authorize` - - Token endpoint URL: `https://login.microsoftonline.com//oauth2/v2.0/token` - - Default scope: `api:///api.access` (or your API scope) -4. Save the OAuth 2.0 server. -5. Open your API in APIM -> Settings and attach this OAuth 2.0 server under Security if you want Developer Portal Authorize support. -6. Open your API -> Design -> Inbound processing and ensure the `validate-jwt` policy above is present. - -> [!NOTE] -> APIM OAuth server configuration enables the Authorize experience; token acceptance is still controlled by the API policy (`validate-jwt`). - -### 6) Test APIM policy after configuration - -**Rule: validate both positive and negative paths to confirm `validate-jwt` is enforcing audience and role correctly.** - -Set your test variables first: - -```bash -APIM_BASE="https://.azure-api.net/" -APIM_SUB_KEY="" -TENANT_ID="" -APIM_APP_ID="" -``` - -#### 6a) Positive test: ACA managed identity (or equivalent caller) succeeds - -```bash -# This token should be requested for APIM audience: api:///.default -TOKEN="" - -curl -i "$APIM_BASE/health" \ - -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" \ - -H "Authorization: Bearer $TOKEN" -``` - -Expected result: - -- `200` (or your API's expected success code) -- No `Unauthorized. Missing or invalid token.` message - -#### 6b) Negative test: no token should fail - -```bash -curl -i "$APIM_BASE/health" \ - -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" -``` - -Expected result: - -- `401 Unauthorized` -- Error from `validate-jwt` policy - -#### 6c) Negative test: wrong audience should fail - -```bash -# Use a token for ACA audience instead of APIM audience. -BAD_TOKEN="" - -curl -i "$APIM_BASE/health" \ - -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" \ - -H "Authorization: Bearer $BAD_TOKEN" -``` - -Expected result: - -- `401 Unauthorized` -- Audience validation failure - -#### 6d) Negative test: missing role should fail - -```bash -# Use a token that has APIM audience but lacks roles: API.Caller. -NO_ROLE_TOKEN="" - -curl -i "$APIM_BASE/health" \ - -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" \ - -H "Authorization: Bearer $NO_ROLE_TOKEN" -``` - -Expected result: - -- `401 Unauthorized` -- Required claim (`roles=API.Caller`) validation failure - -> [!TIP] -> For fast diagnosis, temporarily project token claims in APIM trace and verify `aud`, `iss`, and `roles` match your `validate-jwt` policy. - -## Full flow - -```mermaid -flowchart LR - A["Client App Registration
SimpleL7Proxy-Client"] -->|"Token aud=api://ACA_APP_ID"| B["ACA Ingress + Easy Auth"] - B -->|"Validates ACA audience"| C["SimpleL7Proxy in ACA"] - C -->|"Managed identity token
aud=api://APIM_APP_ID/.default"| D["APIM"] - D -->|"validate-jwt: audience + role API.Caller"| E["Backend routing/policy"] - - F["ACA API App Registration
SimpleL7Proxy-ACA-API"] -. "Defines audience and scope" .-> B - G["APIM API App Registration
SimpleL7Proxy-APIM-API"] -. "Defines API.Caller role" .-> D -``` - -## Worked example - -| Step | Example value | Result | -| :--- | :--- | :--- | -| Create APIM API app registration | `appId = 11111111-1111-1111-1111-111111111111` | APIM audience becomes `api://11111111-1111-1111-1111-111111111111` | -| Create ACA API app registration | `appId = 22222222-2222-2222-2222-222222222222` | ACA audience becomes `api://22222222-2222-2222-2222-222222222222` | -| Create client app registration | `appId = 33333333-3333-3333-3333-333333333333` | Client can request token for ACA audience | -| Assign ACA MI role on APIM API | `New-MgServicePrincipalAppRoleAssignment ...` | APIM accepts ACA token with `roles: API.Caller` | -| Request token in ACA for APIM | `resource = api://111.../.default` | ACA -> APIM call authorized | - -## Verify - -- [ ] APIM API app registration exists with app role `API.Caller`. -- [ ] ACA API app registration exists with scope `api.access` and identifier URI. -- [ ] Client app registration has permission to call ACA API. -- [ ] ACA managed identity is assigned to APIM API app role. -- [ ] ACA auth is enabled and configured with ACA audience. -- [ ] APIM `validate-jwt` checks APIM audience and `roles=API.Caller`. -- [ ] Client can call ACA with token for ACA audience. -- [ ] ACA can call APIM with managed identity token for APIM audience. - -## Related docs - -- [scripts/README.md](../scripts/README.md) -- [scripts/ca2apimSetup.sh](../scripts/ca2apimSetup.sh) -- [scripts/console2caSetup.sh](../scripts/console2caSetup.sh) -- [scripts/enableContainerAppAuth.sh](../scripts/enableContainerAppAuth.sh) -- [APIM-Policy/readme.md](../APIM-Policy/readme.md) +- ACA and APIM have different enforcement points and token audiences. +- Splitting reduces setup confusion and makes validation steps clearer. +- Each document now has a dedicated diagram, worked example, and verification checklist. From b65e82e5d0407022ca922b88c2d5eb3423a483e1 Mon Sep 17 00:00:00 2001 From: MarvelintheCloud Date: Thu, 21 May 2026 20:48:49 -0500 Subject: [PATCH 6/6] Expand APIM security POC with MI setup, issuer guidance, and operational tests --- docs/POC-APIM-Security-Authorization.md | 54 ++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/docs/POC-APIM-Security-Authorization.md b/docs/POC-APIM-Security-Authorization.md index 2afc8ff..f4a8b82 100644 --- a/docs/POC-APIM-Security-Authorization.md +++ b/docs/POC-APIM-Security-Authorization.md @@ -29,6 +29,20 @@ ## Setup +### 0) Enable system-assigned managed identity on ACA + +**Rule: ACA managed identity `principalId` is the identity that must receive `API.Caller` on the APIM API app.** + +```bash +RG="" +CA_NAME="" + +az containerapp identity assign -g "$RG" -n "$CA_NAME" --system-assigned + +ACA_MANAGED_IDENTITY_OBJECT_ID="$(az containerapp show -g "$RG" -n "$CA_NAME" --query "identity.principalId" -o tsv)" +echo "$ACA_MANAGED_IDENTITY_OBJECT_ID" +``` + ### 1) Create APIM API app registration in Entra **Rule: APIM policy audience must match this app's identifier URI.** @@ -85,8 +99,10 @@ New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $acaSpId -PrincipalI api:// + + https://sts.windows.net// https://login.microsoftonline.com//v2.0 @@ -101,6 +117,9 @@ New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $acaSpId -PrincipalI > [!TIP] > If your APIM endpoint accepts delegated user tokens, validate `scp` instead of `roles`. +> [!NOTE] +> Match the issuer to the token you actually receive. Depending on flow and token version, `iss` may be `https://sts.windows.net//` or `https://login.microsoftonline.com//v2.0`. + ### 4) Configure OAuth 2.0 in APIM interface **Rule: OAuth server settings support authorize/testing UX, while `validate-jwt` policy remains the true gate.** @@ -147,12 +166,13 @@ Set variables: ```bash APIM_BASE="https://.azure-api.net/" APIM_SUB_KEY="" +APIM_APP_ID="" ``` Positive test: ```bash -TOKEN="" +TOKEN="$(az account get-access-token --resource "api://$APIM_APP_ID" --query accessToken -o tsv)" curl -i "$APIM_BASE/health" \ -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" \ -H "Authorization: Bearer $TOKEN" @@ -171,7 +191,7 @@ Expected: `401 Unauthorized`. Negative test (wrong audience): ```bash -BAD_TOKEN="" +BAD_TOKEN="$(az account get-access-token --resource "https://management.azure.com/" --query accessToken -o tsv)" curl -i "$APIM_BASE/health" \ -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" \ -H "Authorization: Bearer $BAD_TOKEN" @@ -190,6 +210,26 @@ curl -i "$APIM_BASE/health" \ Expected: `401 Unauthorized` due to missing required claim. +> [!NOTE] +> Role assignment changes are not retroactive to already-issued tokens. If you revoke a role, old tokens may continue to work until token expiry. + +## Optional: proxy -> APIM with managed identity + +**Rule: when proxy host config uses `usemi=true`, set `audience` so ACA requests the correct APIM token.** + +```bash +Host1="host=https://.azure-api.net;usemi=true;audience=api://;probe=/health" +``` + +Runtime behavior: + +- Proxy acquires a managed identity token for `api://`. +- Proxy forwards requests to APIM with `Authorization: Bearer `. +- APIM `validate-jwt` evaluates that token before backend routing. + +> [!WARNING] +> If `usemi=true` and `audience` is missing or incorrect, APIM receives an invalid or missing token and returns `401`. + ## Verify - [ ] APIM API app registration exists with identifier URI `api://`. @@ -199,6 +239,16 @@ Expected: `401 Unauthorized` due to missing required claim. - [ ] Valid APIM token succeeds. - [ ] Missing token, wrong audience, and missing role all fail with `401`. +## Troubleshooting + +| Symptom | Likely cause | Check | +| :--- | :--- | :--- | +| `401` with token that seems valid | Token audience mismatch | Decode token and verify `aud` equals `api://` or configured accepted value | +| `401` with correct audience | Missing `roles` claim | Verify role assignment to ACA managed identity on APIM API service principal | +| `401` only from ACA path but local test succeeds | Wrong ACA principal or missing role assignment | Confirm `identity.principalId` on ACA matches assigned principal | +| `401` with signature or issuer validation issues | Issuer mismatch in policy | Compare token `iss` to policy `` entries | +| Role removed but calls still succeed | Old token still valid | Wait for token expiry, then retest with a new token | + ## Related docs - [scripts/README.md](../scripts/README.md)