diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json
index b3c4037a..ed7d6bc5 100644
--- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json
+++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json
@@ -218,6 +218,13 @@
"type": "String",
"value": "changeme-oauth"
},
+ "scimCredentialLocationInRequest": {
+ "metadata": {
+ "description": "Required OAuth2 client credential location: Header or Body (used with OAuth2ClientCredentialsGrant)"
+ },
+ "type": "String",
+ "value": "Header"
+ },
"scimContentType": {
"metadata": {
"description": "Content-Type header for SCIM API calls"
@@ -231,7 +238,7 @@
},
"scimOAuthScope": {
"metadata": {
- "description": "OAuth2 scope for token acquisition (optional)"
+ "description": "Optional space-delimited OAuth2 scopes for token acquisition"
},
"type": "String",
"value": ""
diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIM-Validation-Test-Overview.md b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIM-Validation-Test-Overview.md
index 9d72bd45..3a05925a 100644
--- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIM-Validation-Test-Overview.md
+++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIM-Validation-Test-Overview.md
@@ -67,7 +67,7 @@ Tests that don't apply are reported as **SKIPPED** (not failures). However, grou
| 18 | **SCIM_User_Update_Test** | mandatory | Direct `PATCH /Users/{id}` with auto-generated update values for eligible attributes (single-valued, string/integer/datetime/boolean, excluding groups/roles/id/schemas/meta/active). | PATCH returns 200/204; subsequent GET returns 200/204; all PATCHed attribute values match in the response; zero mismatches. Passes with `(no updatable attributes)` if no eligible attributes exist. |
| 19 | **SCIM_Group_Create_Test** | mandatory | Direct `POST /Groups` with SCIM group body. **FAILED** if groups not supported. | HTTP 200 or 201; response contains non-empty `id`; returned `displayName` matches sent `displayName`. |
| 20 | **SCIM_Group_Update_Test** | mandatory | Direct `PATCH /Groups/{id}` with attribute updates (single-valued, non-reference, excluding id/members/schemas/meta). | PATCH returns 200/204; GET returns 200/204; all PATCHed attributes match; zero mismatches. |
-| 21 | **Validate_Credentials_Test** | mandatory | OAuth 2.0 Client Credentials flow — acquires token from ISV's `scimTokenEndpoint`, then validates SCIM access with the token. **Skipped** when `scimTokenEndpoint` is empty (static bearer token). | Token endpoint returns 2xx with `access_token`; token validity between 60–360 minutes; SCIM endpoint returns 2xx when called with the token. |
+| 21 | **Validate_Credentials_Test** | mandatory | OAuth 2.0 Client Credentials flow — acquires a token from the ISV's `scimTokenEndpoint`, sends credentials in the required `Header` or `Body` location, includes the optional space-delimited scope string, then validates SCIM access with the token. **Skipped** when `scimTokenEndpoint` is empty (static bearer token). | Token endpoint returns 2xx with `access_token`; token validity between 60–360 minutes; SCIM endpoint returns 2xx when called with the token. |
| 22 | **Federated_Identity_Test** | mandatory | Workload identity federation — acquires Entra ID token, then exchanges it for ISV token via one of three flows: Google Service Account, Google STS, or generic federated endpoint. **Skipped** when federated parameters are empty. | Entra token acquisition returns 2xx with `access_token`; federated token exchange returns 2xx with valid token. Supports Google SA flow (STS exchange → SA impersonation), Google STS flow (direct exchange), and generic flow (client assertion). |
| 23 | **SCIM_User_Pagination_Test** | mandatory | Ensures ≥11 users exist (creates throwaway users if needed), then paginates `GET /Users?startIndex=N&count=5` across pages. Cleans up created users afterward. | At least 2 pages traversed; all pages return HTTP 200; response `startIndex` matches expected value (`page * 5 + 1`); zero failed page verifications. |
| 24 | **SCIM_Group_Pagination_Test** | optional | Same as user pagination but for `/Groups`. Failure = WARNING. **Skipped** if groups not supported. | At least 2 pages traversed; all pages return HTTP 200; response `startIndex` matches expected; zero failed verifications. |
diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_Workflow.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_Workflow.json
index be7993bd..690ec0a4 100644
--- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_Workflow.json
+++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_Workflow.json
@@ -3462,10 +3462,8 @@
"inputs": {
"uri": "@{parameters('scimTokenEndpoint')}",
"method": "POST",
- "headers": {
- "Content-Type": "application/x-www-form-urlencoded"
- },
- "body": "@{concat('grant_type=client_credentials&client_id=', encodeUriComponent(parameters('scimClientId')), '&client_secret=', encodeUriComponent(parameters('scimClientSecret')), if(empty(coalesce(parameters('scimOAuthScope'), '')), '', concat('&scope=', encodeUriComponent(parameters('scimOAuthScope')))))}"
+ "headers": "@if(equals(toLower(coalesce(parameters('scimCredentialLocationInRequest'), 'Header')), 'header'), json(concat('{\"Content-Type\":\"application/x-www-form-urlencoded\",\"Authorization\":\"Basic ', base64(concat(encodeUriComponent(parameters('scimClientId')), ':', encodeUriComponent(parameters('scimClientSecret')))), '\"}')), json('{\"Content-Type\":\"application/x-www-form-urlencoded\"}'))",
+ "body": "@{concat('grant_type=client_credentials', if(equals(toLower(coalesce(parameters('scimCredentialLocationInRequest'), 'Header')), 'body'), concat('&client_id=', encodeUriComponent(parameters('scimClientId')), '&client_secret=', encodeUriComponent(parameters('scimClientSecret'))), ''), if(empty(coalesce(parameters('scimOAuthScope'), '')), '', concat('&scope=', encodeUriComponent(parameters('scimOAuthScope')))))}"
}
},
"Check_Token_Response": {
diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md
index 7270737a..44b27ece 100644
--- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md
+++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md
@@ -408,12 +408,15 @@ hours.
will note that `Validate_Credentials_Test` will be skipped (this is
expected).
- **OAuth client credentials** — If you choose this, the agent will ask
- 4 follow-up questions:
+ 5 follow-up questions:
- **Client ID** — Your OAuth application’s client ID
- **Client Secret** — Your OAuth application’s client secret
- **Token Endpoint URL** — e.g., `https://auth.myapp.com/oauth/token`
- - **OAuth Scope** — The scope required for SCIM access (leave empty if
- not applicable)
+ - **How credentials are sent** — Required; select **Header** (the
+ default, using HTTP Basic authentication) or **Body** (`client_id`
+ and `client_secret` form fields)
+ - **OAuth Scope** — Optional; enter multiple scopes as one string
+ separated by spaces, or enter `none`
### Question 4: Azure Subscription Selection
@@ -1003,6 +1006,10 @@ style="width:4.43812in;height:2.54202in" />
+Update `scimCredentialLocationInRequest` with `Header` or `Body`.
+For OAuth endpoints that require scopes, update `scimOAuthScope` with
+the space-delimited scope string.
+
## Run the Logic App
34. You’re now ready to run the Logic app! Navigate to **WorkFlows\>**
diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION
index d7213f31..f0750610 100644
--- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION
+++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION
@@ -1 +1 @@
-12.0
+13.0
diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md
index afc1475d..68db78b5 100644
--- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md
+++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md
@@ -54,19 +54,22 @@ Collect the ISV's SCIM endpoint and bearer token, validate their Azure environme
b. **Bearer token** (must be long-lived — warn if JWT expires within 2 hours)
c. **Authentication method** — ask: "Does your SCIM endpoint use OAuth client credentials or a static bearer token?"
- - If **OAuth**: ask for all 4 parameters, one at a time:
+ - If **OAuth**: ask for all 5 parameters, one at a time:
- Client ID
- Client Secret
- Token endpoint URL (e.g., `https://auth.example.com/oauth/token`)
+ - How credentials are sent — **MANDATORY:** use `ask_user` with exactly two options: `Header` and `Body`. Set `Header` as the suggested default, but require the customer to confirm their selection.
+ - `Header` sends the client ID and secret using HTTP Basic authentication and omits them from the form body.
+ - `Body` sends `client_id` and `client_secret` in the form body.
- OAuth scope — **MANDATORY: use this EXACT prompt text in `ask_user`, do NOT paraphrase, do NOT use the words "leave blank", "leave empty", "optional", or "if not required":**
- > **OAuth scope** (e.g., `https://graph.microsoft.com/.default`).
+ > **OAuth scope** (e.g., `https://graph.microsoft.com/.default`). Enter multiple scopes as one string separated by spaces.
>
> ⚠️ If your token endpoint does NOT require a scope, type the word `none` (without quotes) and press Enter.
>
> Do NOT submit an empty box — empty submissions are treated as cancellation and the agent will stop.
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 expected test behavior.)
+ - If **static bearer token**: record `authMethod = bearer`. The 5 OAuth fields (`scimClientId`, `scimClientSecret`, `scimTokenEndpoint`, `scimCredentialLocationInRequest`, `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 expected test behavior.)
d. **Federated identity test inputs (for `Federated_Identity_Test`)**:
- Ask: "Do you want to run the federated identity validation test now?"
@@ -216,6 +219,8 @@ Extract from response:
| Bearer only | A | Bearer | `authenticationType` + `baseAddress` + `secretToken` |
| Bearer + OAuth | B | OAuth | `authenticationType` + `baseAddress` + `oauth2ClientId` + `oauth2ClientSecret` + `oauth2TokenExchangeUri` + `credentialLocationInRequest` |
+For Branch B, also include the optional lower-case `scope` connectivity parameter when the ISV provided scopes. Omit `scope` when the ISV answered `none`.
+
---
**Sub-step 2b-1: validateCredentials (bearer — ALWAYS run this)**
@@ -275,7 +280,8 @@ az rest --method PATCH \
# {"key":"oauth2ClientId","value":""},
# {"key":"oauth2ClientSecret","value":""},
# {"key":"oauth2TokenExchangeUri","value":""},
-# {"key":"credentialLocationInRequest","value":"Header"}
+# {"key":"credentialLocationInRequest","value":""},
+# {"key":"scope","value":""}
# ]}
az rest --method PATCH \
--url "https://graph.microsoft.com/beta/servicePrincipals//synchronization/connectivityParameters" \
@@ -285,6 +291,8 @@ az rest --method PATCH \
> Note: `authenticationType=OAuth2ClientCredentialsGrant` is required for OAuth — without it, Graph won't attempt OAuth token acquisition from the stored creds.
>
+> `credentialLocationInRequest` is required for OAuth and must be exactly `Header` or `Body`, matching the ISV's Phase 1 answer. Add the `scope` entry only when the ISV provided one or more scopes; omit it when the ISV answered `none`. Preserve multiple scopes as one space-delimited string.
+>
> Inline `validateCredentials` with OAuth keys (i.e. `useSavedCredentials: false` + credentials array including OAuth keys) **works**, but do **not** include `CredentialLocationInRequest` in that inline payload — Graph returns `InternalError: Requested value 'CredentialLocationInRequest' was not found.` Keep that key only in the beta `connectivityParameters` PATCH payload.
Then validate saved credentials work (both branches) — **this is the Test Connection**:
@@ -303,7 +311,7 @@ az rest --method POST \
|---|---|---|
| HTTP 200/204 (empty body) | Test Connection succeeded — Entra can reach the ISV's SCIM endpoint using the saved credentials | Proceed to 2b-3 |
| HTTP 400 `CredentialValidationUnavailable` | **Branch A:** bearer token rejected by the ISV's SCIM server (401/403/5xx). **Branch B:** OAuth token exchange failed — wrong client ID, wrong client secret, bad token endpoint URL, scope issue, or the ISV's token endpoint issued a token that their SCIM server rejected. | Surface the inner error verbatim to the ISV. **ABORT.** Do NOT create the sync job — it will immediately quarantine. Wait for the ISV to provide corrected credentials, then re-PATCH `connectivityParameters` and re-validate. |
-| HTTP 500 `InternalError` — `"Requested value 'X' was not found"` | Inline validation payload included an unsupported key (for example `CredentialLocationInRequest`) or the connectivity parameters used the wrong key casing | Re-run inline validation with only the supported OAuth keys, then re-PATCH `connectivityParameters` using the lower-case portal keys (`authenticationType`, `baseAddress`, `oauth2ClientId`, `oauth2ClientSecret`, `oauth2TokenExchangeUri`, `credentialLocationInRequest`). Then re-validate. |
+| HTTP 500 `InternalError` — `"Requested value 'X' was not found"` | Inline validation payload included an unsupported key (for example `CredentialLocationInRequest`) or the connectivity parameters used the wrong key casing | Re-run inline validation with only the supported OAuth keys, then re-PATCH `connectivityParameters` using the lower-case portal keys (`authenticationType`, `baseAddress`, `oauth2ClientId`, `oauth2ClientSecret`, `oauth2TokenExchangeUri`, `credentialLocationInRequest`, plus `scope` only when provided). Then re-validate. |
| Any other 4xx/5xx | Unexpected | Surface verbatim and **ABORT**. |
Before aborting or retrying, capture the exact inner error and match it against **Step 6d: Known Issues**. If the response mentions `SystemForCrossDomainIdentityManagementServiceIncompatible`, Group connectivity, or an inner Group `404`, evaluate **Pattern #15**. Do not classify a generic `401`, `403`, rejected bearer token, or OAuth token-exchange failure as Pattern #15.
@@ -356,7 +364,8 @@ else:
| `scimClientId` | `""` | `` |
| `scimClientSecret` | `""` | `` |
| `scimTokenEndpoint` | `""` | `` |
-| `scimOAuthScope` | `""` | `` |
+| `scimCredentialLocationInRequest` | `""` | `` |
+| `scimOAuthScope` | `""` | `` |
For federated identity testing, also pass these Phase 4 parameters:
@@ -822,7 +831,7 @@ Pass criteria (ALL must hold):
- `schedule.state` is `Active`
If `status.code == Quarantine` with `lastExecError == SystemForCrossDomainIdentityManagementInvalidCredentials` and `lastExecMsg` mentions `BaseAddress`/`SecretToken`/credential, the Step 2b connectivity parameters were rejected or incomplete. Recovery:
-1. Re-PATCH `https://graph.microsoft.com/beta/servicePrincipals//synchronization/connectivityParameters` with ONLY the supported keys for the auth mode (bearer: `authenticationType` + `baseAddress` + `secretToken`; OAuth: `authenticationType` + `baseAddress` + `oauth2ClientId` + `oauth2ClientSecret` + `oauth2TokenExchangeUri` + `credentialLocationInRequest`).
+1. Re-PATCH `https://graph.microsoft.com/beta/servicePrincipals//synchronization/connectivityParameters` with ONLY the supported keys for the auth mode (bearer: `authenticationType` + `baseAddress` + `secretToken`; OAuth: `authenticationType` + `baseAddress` + `oauth2ClientId` + `oauth2ClientSecret` + `oauth2TokenExchangeUri` + `credentialLocationInRequest`, plus `scope` only when provided).
2. `POST /servicePrincipals//synchronization/jobs//restart` with body `{"criteria":{"resetScope":"Full"}}`. (Note: `credentials`/`watermark`/`escrows`/`quarantineState` are NOT valid restart criteria properties — schema only allows `resetScope`.)
3. `POST /jobs//start` again.
4. Re-run this Step 3h check. If still quarantined, abort and report the exact error.
@@ -907,7 +916,8 @@ Update these parameters in the JSON:
| `scimClientId` | `` | From Phase 1 — set if the ISV provided OAuth credentials. Used by the LA's `Validate_Credentials_Test` to exercise the OAuth flow independently. Empty string if not provided. **Note: Entra sync always uses bearer token (Step 2b), NOT these OAuth values.** |
| `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). |
+| `scimCredentialLocationInRequest` | `` | From Phase 1 — mandatory when OAuth client credentials are provided. Empty string for static bearer authentication. |
+| `scimOAuthScope` | `` | From Phase 1 — optional; preserve multiple scopes as one space-delimited string. Empty string if the ISV answered `none`. |
| `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. |
@@ -924,7 +934,8 @@ 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,
+scimCredentialLocationInRequest, scimOAuthScope,
federatedEntraTenantId, federatedApplicationId, federatedApplicationClientSecret,
federatedTokenEndpoint, federatedClientId, federatedBaseAddress, federatedAudience
```
@@ -935,7 +946,7 @@ If any key is missing, abort Phase 4 and tell the ISV exactly which key is missi
After the PUT completes, re-GET `parameters.json` and for each key in the patch table above, assert the returned value matches what was sent. If `servicePrincipalId` was supposed to be `aaa-bbb-ccc` but the read-back shows something else (or the key is missing), abort Phase 4 with the specific mismatch. Do NOT proceed to Phase 5.
-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 OAuth credentials, leave `scimClientId`, `scimClientSecret`, `scimTokenEndpoint`, `scimCredentialLocationInRequest`, 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`).