Skip to content

Commit 8bdf02e

Browse files
ggarima01Copilot
andauthored
Support OAuth client credential location and scopes (#169)
* Support OAuth client credential location and scopes Adds OAuth 2.0 client credentials configuration for SCIM onboarding. Customers can select whether credentials are sent through the HTTP Basic Authorization header or request body, with Header as the default. Customers can also provide an optional space-delimited OAuth scope string. Updates the Logic App OAuth validation request, Entra connectivity parameters, onboarding agent instructions, setup documentation, validation overview, and bumps the Standard Logic App version to 13.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Encode OAuth Basic credentials per RFC 6749 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5c32705b-7e16-4b9e-8061-95f225ffc593 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Garima Garg <ggarima01@users.noreply.github.com> Copilot-Session: 5c32705b-7e16-4b9e-8061-95f225ffc593
1 parent 67858cb commit 8bdf02e

6 files changed

Lines changed: 43 additions & 20 deletions

File tree

‎Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,13 @@
218218
"type": "String",
219219
"value": "changeme-oauth"
220220
},
221+
"scimCredentialLocationInRequest": {
222+
"metadata": {
223+
"description": "Required OAuth2 client credential location: Header or Body (used with OAuth2ClientCredentialsGrant)"
224+
},
225+
"type": "String",
226+
"value": "Header"
227+
},
221228
"scimContentType": {
222229
"metadata": {
223230
"description": "Content-Type header for SCIM API calls"
@@ -231,7 +238,7 @@
231238
},
232239
"scimOAuthScope": {
233240
"metadata": {
234-
"description": "OAuth2 scope for token acquisition (optional)"
241+
"description": "Optional space-delimited OAuth2 scopes for token acquisition"
235242
},
236243
"type": "String",
237244
"value": ""

‎Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIM-Validation-Test-Overview.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Tests that don't apply are reported as **SKIPPED** (not failures). However, grou
6767
| 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. |
6868
| 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`. |
6969
| 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. |
70-
| 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. |
70+
| 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. |
7171
| 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). |
7272
| 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. |
7373
| 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. |

‎Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_Workflow.json‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3462,10 +3462,8 @@
34623462
"inputs": {
34633463
"uri": "@{parameters('scimTokenEndpoint')}",
34643464
"method": "POST",
3465-
"headers": {
3466-
"Content-Type": "application/x-www-form-urlencoded"
3467-
},
3468-
"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')))))}"
3465+
"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\"}'))",
3466+
"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')))))}"
34693467
}
34703468
},
34713469
"Check_Token_Response": {

‎Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -408,12 +408,15 @@ hours.
408408
will note that `Validate_Credentials_Test` will be skipped (this is
409409
expected).
410410
- **OAuth client credentials** — If you choose this, the agent will ask
411-
4 follow-up questions:
411+
5 follow-up questions:
412412
- **Client ID** — Your OAuth application’s client ID
413413
- **Client Secret** — Your OAuth application’s client secret
414414
- **Token Endpoint URL** — e.g., `https://auth.myapp.com/oauth/token`
415-
- **OAuth Scope** — The scope required for SCIM access (leave empty if
416-
not applicable)
415+
- **How credentials are sent** — Required; select **Header** (the
416+
default, using HTTP Basic authentication) or **Body** (`client_id`
417+
and `client_secret` form fields)
418+
- **OAuth Scope** — Optional; enter multiple scopes as one string
419+
separated by spaces, or enter `none`
417420

418421
### Question 4: Azure Subscription Selection
419422

@@ -1003,6 +1006,10 @@ style="width:4.43812in;height:2.54202in" />
10031006
<img src="./media/image63.png"
10041007
style="width:4.40686in;height:2.71913in" />
10051008

1009+
Update `scimCredentialLocationInRequest` with `Header` or `Body`.
1010+
For OAuth endpoints that require scopes, update `scimOAuthScope` with
1011+
the space-delimited scope string.
1012+
10061013
## Run the Logic App
10071014

10081015
34. You’re now ready to run the Logic app! Navigate to **WorkFlows\>**
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
12.0
1+
13.0

‎Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md‎

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,22 @@ Collect the ISV's SCIM endpoint and bearer token, validate their Azure environme
5454
b. **Bearer token** (must be long-lived — warn if JWT expires within 2 hours)
5555

5656
c. **Authentication method** — ask: "Does your SCIM endpoint use OAuth client credentials or a static bearer token?"
57-
- If **OAuth**: ask for all 4 parameters, one at a time:
57+
- If **OAuth**: ask for all 5 parameters, one at a time:
5858
- Client ID
5959
- Client Secret
6060
- Token endpoint URL (e.g., `https://auth.example.com/oauth/token`)
61+
- 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.
62+
- `Header` sends the client ID and secret using HTTP Basic authentication and omits them from the form body.
63+
- `Body` sends `client_id` and `client_secret` in the form body.
6164
- 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":**
62-
> **OAuth scope** (e.g., `https://graph.microsoft.com/.default`).
65+
> **OAuth scope** (e.g., `https://graph.microsoft.com/.default`). Enter multiple scopes as one string separated by spaces.
6366
>
6467
> ⚠️ If your token endpoint does NOT require a scope, type the word `none` (without quotes) and press Enter.
6568
>
6669
> Do NOT submit an empty box — empty submissions are treated as cancellation and the agent will stop.
6770
6871
The agent treats `none` (case-insensitive) as an empty scope when writing `scimOAuthScope` to `parameters.json`.
69-
- 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.)
72+
- 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.)
7073

7174
d. **Federated identity test inputs (for `Federated_Identity_Test`)**:
7275
- Ask: "Do you want to run the federated identity validation test now?"
@@ -216,6 +219,8 @@ Extract from response:
216219
| Bearer only | A | Bearer | `authenticationType` + `baseAddress` + `secretToken` |
217220
| Bearer + OAuth | B | OAuth | `authenticationType` + `baseAddress` + `oauth2ClientId` + `oauth2ClientSecret` + `oauth2TokenExchangeUri` + `credentialLocationInRequest` |
218221

222+
For Branch B, also include the optional lower-case `scope` connectivity parameter when the ISV provided scopes. Omit `scope` when the ISV answered `none`.
223+
219224
---
220225

221226
**Sub-step 2b-1: validateCredentials (bearer — ALWAYS run this)**
@@ -275,7 +280,8 @@ az rest --method PATCH \
275280
# {"key":"oauth2ClientId","value":"<clientId>"},
276281
# {"key":"oauth2ClientSecret","value":"<clientSecret>"},
277282
# {"key":"oauth2TokenExchangeUri","value":"<tokenEndpoint>"},
278-
# {"key":"credentialLocationInRequest","value":"Header"}
283+
# {"key":"credentialLocationInRequest","value":"<Header-or-Body>"},
284+
# {"key":"scope","value":"<space-delimited-scopes>"}
279285
# ]}
280286
az rest --method PATCH \
281287
--url "https://graph.microsoft.com/beta/servicePrincipals/<servicePrincipalId>/synchronization/connectivityParameters" \
@@ -285,6 +291,8 @@ az rest --method PATCH \
285291

286292
> Note: `authenticationType=OAuth2ClientCredentialsGrant` is required for OAuth — without it, Graph won't attempt OAuth token acquisition from the stored creds.
287293
>
294+
> `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.
295+
>
288296
> 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.
289297
290298
Then validate saved credentials work (both branches) — **this is the Test Connection**:
@@ -303,7 +311,7 @@ az rest --method POST \
303311
|---|---|---|
304312
| HTTP 200/204 (empty body) | Test Connection succeeded — Entra can reach the ISV's SCIM endpoint using the saved credentials | Proceed to 2b-3 |
305313
| 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. |
306-
| 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. |
314+
| 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. |
307315
| Any other 4xx/5xx | Unexpected | Surface verbatim and **ABORT**. |
308316

309317
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:
356364
| `scimClientId` | `""` | `<clientId>` |
357365
| `scimClientSecret` | `""` | `<clientSecret>` |
358366
| `scimTokenEndpoint` | `""` | `<tokenEndpoint>` |
359-
| `scimOAuthScope` | `""` | `<scope>` |
367+
| `scimCredentialLocationInRequest` | `""` | `<Header-or-Body>` |
368+
| `scimOAuthScope` | `""` | `<space-delimited-scopes-or-empty-string>` |
360369

361370
For federated identity testing, also pass these Phase 4 parameters:
362371

@@ -822,7 +831,7 @@ Pass criteria (ALL must hold):
822831
- `schedule.state` is `Active`
823832
824833
If `status.code == Quarantine` with `lastExecError == SystemForCrossDomainIdentityManagementInvalidCredentials` and `lastExecMsg` mentions `BaseAddress`/`SecretToken`/credential, the Step 2b connectivity parameters were rejected or incomplete. Recovery:
825-
1. Re-PATCH `https://graph.microsoft.com/beta/servicePrincipals/<servicePrincipalId>/synchronization/connectivityParameters` with ONLY the supported keys for the auth mode (bearer: `authenticationType` + `baseAddress` + `secretToken`; OAuth: `authenticationType` + `baseAddress` + `oauth2ClientId` + `oauth2ClientSecret` + `oauth2TokenExchangeUri` + `credentialLocationInRequest`).
834+
1. Re-PATCH `https://graph.microsoft.com/beta/servicePrincipals/<servicePrincipalId>/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).
826835
2. `POST /servicePrincipals/<sp>/synchronization/jobs/<jobId>/restart` with body `{"criteria":{"resetScope":"Full"}}`. (Note: `credentials`/`watermark`/`escrows`/`quarantineState` are NOT valid restart criteria properties — schema only allows `resetScope`.)
827836
3. `POST /jobs/<jobId>/start` again.
828837
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:
907916
| `scimClientId` | `<ISV's OAuth client ID>` | 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.** |
908917
| `scimClientSecret` | `<ISV's OAuth client secret>` | From Phase 1 — same. Empty string if not provided. |
909918
| `scimTokenEndpoint` | `<ISV's OAuth token endpoint>` | From Phase 1 — same. Empty string if not provided. |
910-
| `scimOAuthScope` | `<ISV's OAuth scope>` | From Phase 1 — optional, set if provided (empty string if not). |
919+
| `scimCredentialLocationInRequest` | `<Header-or-Body>` | From Phase 1 — mandatory when OAuth client credentials are provided. Empty string for static bearer authentication. |
920+
| `scimOAuthScope` | `<ISV's space-delimited OAuth scopes>` | From Phase 1 — optional; preserve multiple scopes as one space-delimited string. Empty string if the ISV answered `none`. |
911921
| `federatedEntraTenantId` | `<ISV's Entra tenant ID for federated test>` | From Phase 1 federated inputs — empty string if not provided. |
912922
| `federatedApplicationId` | `<Entra app client ID used for federated flow>` | From Phase 1 federated inputs — empty string if not provided. |
913923
| `federatedApplicationClientSecret` | `<Entra app client secret used for federated flow>` | 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
924934
servicePrincipalId, scimEndpoint, scimBearerToken, scimContentType,
925935
testUserDomain, EnabledTests, IsSoftDeleted,
926936
defaultUserProperties, defaultGroupProperties, scimTargetUserValues,
927-
scimClientId, scimClientSecret, scimTokenEndpoint, scimOAuthScope,
937+
scimClientId, scimClientSecret, scimTokenEndpoint,
938+
scimCredentialLocationInRequest, scimOAuthScope,
928939
federatedEntraTenantId, federatedApplicationId, federatedApplicationClientSecret,
929940
federatedTokenEndpoint, federatedClientId, federatedBaseAddress, federatedAudience
930941
```
@@ -935,7 +946,7 @@ If any key is missing, abort Phase 4 and tell the ISV exactly which key is missi
935946
936947
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.
937948
938-
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.
949+
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.
939950
940951
If the ISV did not provide federated identity inputs, leave all federated parameters as empty strings (`federatedEntraTenantId`, `federatedApplicationId`, `federatedApplicationClientSecret`, `federatedTokenEndpoint`, `federatedClientId`, `federatedBaseAddress`, `federatedAudience`).
941952

0 commit comments

Comments
 (0)