diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_WorkloadIdentityFederation_Setup.md b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_WorkloadIdentityFederation_Setup.md new file mode 100644 index 00000000..01daa44f --- /dev/null +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_WorkloadIdentityFederation_Setup.md @@ -0,0 +1,99 @@ +# Overview + +This documentation focuses on the setup related to Workload Identity Federation authentication method. + +Workload Identity Federation is a way for applications/services to authenticate to cloud resources without using stored secrets (client_secret, certificates, API keys). + +Currently we support two different RFCs : + +- [RFC-7523](https://www.rfc-editor.org/info/rfc7523/) - Default RFC. +- [RFC-8693](https://www.rfc-editor.org/rfc/rfc8693.html) - currently eligible only for Google based applications. + +## Disclaimer +[RFC-8693](https://www.rfc-editor.org/rfc/rfc8693.html) is currently implemented for only Google based applications. Please use [RFC-7523](https://www.rfc-editor.org/info/rfc7523/) for default scenarios. + +## Prerequisites + +Before following the steps in this document, complete the setup in [SetupLogicApp-Standard-Agent](https://github.com/AzureAD/SCIMReferenceCode/blob/master/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md) **up to (but not including)** the [`Run: Steps to Run Logic app`](https://github.com/AzureAD/SCIMReferenceCode/blob/master/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md#run-steps-to-run-logic-app) section. The Workload Identity Federation setup described here is performed in place of (or in addition to) the credential setup in that flow, and the Logic App is then run as described in the parent document. + +## Terminology + +| Term | Meaning | +| --- | --- | +| ISV | Independent Software Vendor — the third-party SaaS application whose SCIM endpoint is being provisioned to (e.g., the application being onboarded to the Microsoft Entra app gallery). | +| WIF | Workload Identity Federation — an authentication pattern that allows a workload to obtain access tokens by presenting a signed assertion instead of a stored client secret. | +| JWT bearer assertion | A signed JSON Web Token presented to the ISV's token endpoint to prove the caller's identity, per [RFC-7523](https://www.rfc-editor.org/info/rfc7523/). | +| JWKS URI | JSON Web Key Set URI — the public endpoint the ISV uses to fetch the keys needed to validate the JWT signature. | +| `iss` / `sub` / `aud` claims | Standard JWT claims identifying the token's issuer, subject, and intended audience respectively. | +| Internal auth app | The Microsoft Entra ID application registration created in the [Setup on Microsoft Entra Id](#setup-on-microsoft-entra-id) section. The Logic App authenticates as this app to obtain the JWT assertion presented to the ISV. | + +## Attributes supported + +Although new attributes can be added in logic app, below list are the current attributes supported for the Workload Identity Federation authentication method : + +| Attribute Name | Description | RFC methods | Required | +| --- | --- | --- | --- | +| `federatedClientId` | The client identifier of the trusted issuer used to sign and present the JWT bearer assertion to the authorization server per RFC-7523. | RFC-7523 | Required for ISVs | +| `federatedTokenEndpoint` | The authorization server's token endpoint that is called to exchange the assertion for an access token. | RFC-7523, RFC-8693 | Required for all | +| `federatedBaseAddress` | The base URL of the target SCIM API that is called with the issued access token to retrieve user or group information. | RFC-7523 | Required for ISVs | +| `federatedApplicationId` | The client ID of the new application registered in Entra ID Enterprise Applications (created in the steps below) to enable the end-to-end workload identity flow. | RFC-7523, RFC-8693 | Required for all | +| `federatedApplicationClientSecret` | The client secret of the new application registered in Entra ID Enterprise Applications (created in the steps below) to enable the end-to-end workload identity flow. | RFC-7523, RFC-8693 | Required for all | +| `federatedAudience` | The audience value required by Google's token exchange flow to scope the issued access token to the intended resource. | RFC-8693 | Required for Google | + +## Setup + +This section covers the setup needed for Workload Identity Federation authentication method to work with [SetupLogicApp-Standard-Agent](https://github.com/AzureAD/SCIMReferenceCode/blob/master/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md). + +This requires us to cover Microsoft EntraId setup alongwith the setup on the ISV portal to allow for this to work end to end. + +### Setup on Microsoft Entra Id + +> **What this section sets up:** the **internal auth app** — a Microsoft Entra ID application registration that the Logic App will authenticate as. The client secret created here is used by the Logic App **internally** to sign in and produce the JWT assertion that is then presented to the ISV. The actual federation trust between Entra ID and the ISV is established in the [Setup on ISV](#setup-on-isv) section below. + +To allow for the tests to run end to end, we need to setup a new application in the Enterprise applications. This requires us to do the following : + +- Go to **App Registrations** on the logic application tenant, and select + New registrations. + +- Once selected, it will open a new window. Enter the name of the new application under Name section and click Register. + +- This will open a new screen with the application information. Please note the ApplicationId here. This maps to the federatedApplicationId as one of the input parameters. + +- Once this is done, go to Manage -> Certificates & secrets section. Here select Client secrets option and select `+ New client secret`. Note the client secret as this is shown only once and we would need this value later to map to federatedApplicationClientSecret. + +- Now we need to set the application url to allow for this to be accessible. + - Go to Expose an API section. + - Click Add next to the Application ID URL. + - This will open a side panel with the Application ID URL populated. Click Save if this looks good. + +- Now we can go to Manifest and under Microsoft Graph App Manifest (New), update requestedAccessTokenVersion to 2. + + +This completes the Entra Side setup for the application. Below section covers how this information can be used to setup on the ISV end. + +### Setup on ISV + +> **What this section sets up:** the **Workload Identity Federation trust** on the ISV side. This is where the ISV is configured to trust JWT assertions issued by the internal auth app created above — no shared secret is exchanged with the ISV. The ISV validates incoming tokens by fetching Entra ID's public keys from the JWKS URI and checking the `iss`, `sub`, and `aud` claims against the values configured here. + +Every ISV has their own setup, and the exact UI and field names differ from vendor to vendor. The table below lists the values most ISVs require and shows where to find each one on the Entra side. The configuration steps on the ISV portal itself are owned by the ISV — refer to their documentation for the precise navigation. + + + +Note : The list below is not comprehensive but covers the parameters most commonly requested by ISVs. Documentation for ISV-side configuration is not maintained here as it varies per vendor. + +| Attribute on ISV | Attribute on Entra side | Location on Entra | +| --- | --- | --- | +| Token Issuer (`iss` claim) | `https://login.microsoftonline.com/` | `tenantId` is found at **App registrations** → app created in [Setup on Microsoft Entra Id](#setup-on-microsoft-entra-id) → **Essentials** → **Directory (tenant) ID**. | +| JWKS URI | `https://login.windows.net//discovery/v2.0/keys` | `tenantId` is found at **App registrations** → app created in [Setup on Microsoft Entra Id](#setup-on-microsoft-entra-id) → **Essentials** → **Directory (tenant) ID**. | +| Subject (`sub` claim) | Object ID | **App registrations** → app created in [Setup on Microsoft Entra Id](#setup-on-microsoft-entra-id) → **Essentials** → select **Managed application in local directory** → copy **Object ID** from the page that loads. | +| Audience (`aud` claim) | Application ID | **App registrations** → app created in [Setup on Microsoft Entra Id](#setup-on-microsoft-entra-id) → **Essentials** → **Application (client) ID**. | + +## Conclusion + +This wraps up the setup required for Workload Identity authentication. Please feel free to go back to [SetupLogicApp-Standard-Agent](https://github.com/AzureAD/SCIMReferenceCode/blob/master/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md). \ No newline at end of file diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image72.png b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image72.png new file mode 100644 index 00000000..a6b5266f Binary files /dev/null and b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image72.png differ diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image73.png b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image73.png new file mode 100644 index 00000000..ca9214ea Binary files /dev/null and b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image73.png differ diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image74.png b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image74.png new file mode 100644 index 00000000..f73ae074 Binary files /dev/null and b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image74.png differ diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image75.png b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image75.png new file mode 100644 index 00000000..cf8db7af Binary files /dev/null and b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image75.png differ diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image76.png b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image76.png new file mode 100644 index 00000000..54dc68ca Binary files /dev/null and b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image76.png differ diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image77.png b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image77.png new file mode 100644 index 00000000..6096c5e8 Binary files /dev/null and b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/media/image77.png differ diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md index cb56a3f5..9ef170e6 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md @@ -68,6 +68,18 @@ Collect the ISV's SCIM endpoint and bearer token, validate their Azure environme The agent treats `none` (case-insensitive) as an empty scope when writing `scimOAuthScope` to `parameters.json`. - If **static bearer token**: record `authMethod = bearer`. The 4 OAuth fields (`scimClientId`, `scimClientSecret`, `scimTokenEndpoint`, `scimOAuthScope`) will be written as empty strings in Phase 4. (Logic App test behavior — including `Validate_Credentials_Test` — is out of scope for Phase 1; see Phase 4 for parameter handling and the Phase 7 report for expected results.) + d. **Federated identity test inputs (for `Federated_Identity_Test`)**: + - Ask: "Do you want to run the federated identity validation test now?" + - If **yes**, collect these values one at a time: + - `federatedEntraTenantId` + - `federatedApplicationId` + - `federatedApplicationClientSecret` + - `federatedTokenEndpoint` + - `federatedClientId` + - `federatedBaseAddress` + - `federatedAudience` (required for Google STS/SA flow, optional otherwise) + - If **no**, set all federated fields to empty strings in Phase 4 so the parameters contract remains complete. + **You MUST ask these questions using `ask_user`. Do NOT assume values, do NOT skip OAuth questions, and do NOT proceed until the ISV has answered.** 3. **Validate Azure CLI login**: @@ -286,39 +298,40 @@ az rest --method POST \ > ⚠️ **GATE: Do NOT proceed to Sub-step 2b-3 until the saved-credential Test Connection returns 200/204.** Creating a sync job with invalid credentials causes immediate quarantine, and recovering from quarantine requires a full restart cycle (re-PUT secrets → restart job with `resetScope: Full` → start → re-verify). It is far cheaper to fix credentials now. -**Sub-step 2b-3: Verify tenant allowlist (check template availability)** - -Before creating the sync job, verify the `isvonboarding` synchronization template is available on the service principal. Template availability is the definitive allowlist gate — if the tenant is not allowlisted, the template will not appear. - +**Sub-step 2b-3: Create the sync job** ```bash -az rest --method GET \ - --url "https://graph.microsoft.com/v1.0/servicePrincipals//synchronization/templates" +az rest --method POST \ + --url "https://graph.microsoft.com/v1.0/servicePrincipals//synchronization/jobs" \ + --body '{"templateId":"isvonboarding"}' ``` +Extract the `jobId` from the response. -Check the response: look for a template with `"id": "isvonboarding"` in the `value` array. +**Sub-step 2b-4: Verify tenant whitelist** -**If the `isvonboarding` template is NOT present (empty `value` array, or no template with that ID):** +The sync job response includes `synchronizationJobSettings` with a `tenantWhitelist` entry — a JSON array of tenant IDs that are authorized to use the ISVOnboarding provisioning features (schema, sync execution, provisionOnDemand). Check if the ISV's tenant is in the list: -> ⚠️ **STOP — do NOT proceed with any further steps (do NOT create the sync job, Logic App, storage account, resource group, or permissions).** The tenant is not allowlisted for the ISVOnboarding provisioning application. The `isvonboarding` synchronization template is not available on this service principal, which means provisioning features (schema, sync execution, provisionOnDemand) will not work. Inform the ISV: +```python +import json +whitelist_str = '' +whitelist = json.loads(whitelist_str) +isv_tenant = '' +if isv_tenant not in whitelist: + print(f"BLOCKED: Tenant {isv_tenant} is not in the whitelist.") +else: + print("Tenant is whitelisted — proceed.") +``` + +**If the ISV's tenant is NOT in the whitelist:** + +> ⚠️ **STOP — do NOT proceed with any further steps (do NOT create the Logic App, storage account, or permissions).** Inform the ISV: > -> *Your tenant is not allowlisted for the ISVOnboarding provisioning application. The `isvonboarding` template is not available. Submit an allowlisting request by completing the form at:* +> *Your tenant is not whitelisted for the ISVOnboarding provisioning application. Provisioning schema, sync execution, and test operations will fail with 401 Unauthorized until your tenant is whitelisted. Submit a whitelisting request by completing the form at:* > > 📄 **https://forms.microsoft.com/pages/responsepage.aspx?id=v4j5cvGGr0GRqy180BHbR3elR4YvzS1IhaP_XITThvJUODY1UTJSSUFXTzFYMTQ0SkxSWTY4OTYzRi4u&route=shorturl** > -> *After submitting, wait for an explicit confirmation from the Microsoft team that your tenant has been allowlisted. Do not attempt to continue until you receive this confirmation. The Entra app has been left in place. Once confirmed, restart the agent — it will detect the existing resources and continue from where it left off.* -> -> Also surface the raw response from the templates endpoint verbatim so the ISV can share it with Microsoft support if needed. +> *After submitting, wait for an explicit confirmation from the Microsoft team that your tenant has been whitelisted. Do not attempt to continue until you receive this confirmation. The Entra app and sync job have been left in place. Once confirmed, restart the agent — it will detect the existing resources and continue from where it left off.* -**If the `isvonboarding` template IS present** → the tenant is allowlisted. Proceed to create the sync job. - -**Sub-step 2b-4: Create the sync job** - -```bash -az rest --method POST \ - --url "https://graph.microsoft.com/v1.0/servicePrincipals//synchronization/jobs" \ - --body '{"templateId":"isvonboarding"}' -``` -Extract the `jobId` from the response. +**If the ISV's tenant IS in the whitelist** → proceed normally. **Do NOT start the provisioning job yet** — the ISV must review attribute mappings first (Phase 3). @@ -333,12 +346,27 @@ Extract the `jobId` from the response. | `scimTokenEndpoint` | `""` | `` | | `scimOAuthScope` | `""` | `` | +For federated identity testing, also pass these Phase 4 parameters: + +| Parameter | Value when provided | Value when not provided | +|---|---|---| +| `federatedEntraTenantId` | `` | `""` | +| `federatedApplicationId` | `` | `""` | +| `federatedApplicationClientSecret` | `` | `""` | +| `federatedTokenEndpoint` | `` | `""` | +| `federatedClientId` | `` | `""` | +| `federatedBaseAddress` | `` | `""` | +| `federatedAudience` | `` | `""` | + +`federatedGrantType` is intentionally omitted because workflow code now derives the grant type. + **Test behavior differences:** | Test | Branch A | Branch B | |---|---|---| -| 22 SCIM tests | Use `scimBearerToken` ✓ | Use `scimBearerToken` ✓ | +| Core SCIM tests | Use `scimBearerToken` ✓ | Use `scimBearerToken` ✓ | | `Validate_Credentials_Test` | **SKIPPED** (empty tokenEndpoint) | **RUNS** (exercises OAuth) | +| `Federated_Identity_Test` | **SKIPPED/FAILED based on missing federated inputs** | **RUNS** when federated inputs are populated | | POD / sync engine | Uses bearer from stored secrets | Uses OAuth from stored secrets | #### Step 2c: Create resource group (if needed) @@ -866,6 +894,13 @@ Update these parameters in the JSON: | `scimClientSecret` | `` | From Phase 1 — same. Empty string if not provided. | | `scimTokenEndpoint` | `` | From Phase 1 — same. Empty string if not provided. | | `scimOAuthScope` | `` | From Phase 1 — optional, set if provided (empty string if not). | +| `federatedEntraTenantId` | `` | From Phase 1 federated inputs — empty string if not provided. | +| `federatedApplicationId` | `` | From Phase 1 federated inputs — empty string if not provided. | +| `federatedApplicationClientSecret` | `` | From Phase 1 federated inputs — empty string if not provided. | +| `federatedTokenEndpoint` | `` | From Phase 1 federated inputs — empty string if not provided. | +| `federatedClientId` | `` | From Phase 1 federated inputs — empty string if not provided. | +| `federatedBaseAddress` | `` | From Phase 1 federated inputs — empty string if not provided. | +| `federatedAudience` | `` | From Phase 1 federated inputs — empty string if not provided. | #### Required-keys check (run BEFORE the PUT) @@ -875,7 +910,9 @@ After patching the JSON in memory, assert every key below exists at the top leve servicePrincipalId, scimEndpoint, scimBearerToken, scimContentType, testUserDomain, EnabledTests, IsSoftDeleted, defaultUserProperties, defaultGroupProperties, scimTargetUserValues, -scimClientId, scimClientSecret, scimTokenEndpoint, scimOAuthScope +scimClientId, scimClientSecret, scimTokenEndpoint, scimOAuthScope, +federatedEntraTenantId, federatedApplicationId, federatedApplicationClientSecret, +federatedTokenEndpoint, federatedClientId, federatedBaseAddress, federatedAudience ``` If any key is missing, abort Phase 4 and tell the ISV exactly which key is missing. Do NOT PUT a parameters.json that fails this check. @@ -886,6 +923,8 @@ After the PUT completes, re-GET `parameters.json` and for each key in the patch If the ISV did not provide OAuth credentials, leave `scimClientId`, `scimClientSecret`, `scimTokenEndpoint`, and `scimOAuthScope` as empty strings. The `Validate_Credentials_Test` will be SKIPPED — note this as expected in the final report. This is unrelated to the Entra sync engine, which always uses the bearer token (`SecretToken`) configured in Step 2b. +If the ISV did not provide federated identity inputs, leave all federated parameters as empty strings (`federatedEntraTenantId`, `federatedApplicationId`, `federatedApplicationClientSecret`, `federatedTokenEndpoint`, `federatedClientId`, `federatedBaseAddress`, `federatedAudience`). + #### defaultUserProperties handling (CRITICAL — do NOT build from scratch) > **NEVER construct `defaultUserProperties` from scratch.** The base `Orchestrator_Parameters.json` template ships 3 user profiles with **all required properties** (25 fields including `displayName`, `hireDate`, nested sub-properties like `employeeOrgData.costCenter` and `passwordProfile.forceChangePasswordNextSignIn`). Building profiles from scratch inevitably misses sub-properties that the workflow template accesses via `coalesce()` / direct property access, causing `InvalidTemplate` errors that only surface one-at-a-time per run. @@ -925,7 +964,7 @@ curl -X PUT \ `Group_Update_Remove_Member_Test`, `POD_Group_Test`, `Restore_Group_Test`, `Schema_Discoverability_Test`, `SCIM_Null_Update_Test`, `SCIM_User_Create_Test`, `SCIM_User_Update_Test`, `SCIM_Group_Create_Test`, `SCIM_Group_Update_Test`, -`SCIM_User_Pagination_Test`, `Validate_Credentials_Test` +`SCIM_User_Pagination_Test`, `SCIM_Group_Pagination_Test`, `Validate_Credentials_Test`, `Federated_Identity_Test` ### Strategy: Run all tests at once Always set `EnabledTests = "All"` on **every** parameters.json write — first run, Phase 6 re-runs, and any retry. The child workflows (UserTests, GroupTests, SCIMTests) execute in parallel, so running all tests does not significantly increase total runtime compared to running subsets. If specific tests fail, the debug flow (Phase 6) identifies and addresses each failure individually — there is no need to gate on earlier tests passing first. @@ -1178,7 +1217,7 @@ Follow this process for **every** failed test in `Final_TestResults`: `Final_TestResults` includes `childWorkflowRunLinks` — a map of workflow name → portal URL. Extract the `runId` from the URL path parameter. Match the failed test to its workflow: - `Create_User_Test`, `Update_User_Test`, `Delete_User_Test`, `Disable_User_Test`, `User_Update_Manager_Test`, `Restore_User_Test`, `POD_User_Test` → **UserTests_Workflow** - `Create_Group_Test`, `Update_Group_Test`, `Delete_Group_Test`, `Group_Update_Add_Member_Test`, `Group_Update_Remove_Member_Test`, `POD_Group_Test`, `Restore_Group_Test` → **GroupTests_Workflow** -- `Schema_Discoverability_Test`, `SCIM_Null_Update_Test`, `SCIM_User_Create_Test`, `SCIM_User_Update_Test`, `SCIM_Group_Create_Test`, `SCIM_Group_Update_Test`, `SCIM_User_Pagination_Test`, `SCIM_Group_Pagination_Test`, `Validate_Credentials_Test` → **SCIMTests_Workflow** +- `Schema_Discoverability_Test`, `SCIM_Null_Update_Test`, `SCIM_User_Create_Test`, `SCIM_User_Update_Test`, `SCIM_Group_Create_Test`, `SCIM_Group_Update_Test`, `SCIM_User_Pagination_Test`, `SCIM_Group_Pagination_Test`, `Validate_Credentials_Test`, `Federated_Identity_Test` → **SCIMTests_Workflow** #### 2. List all executed actions in the child workflow