Conversation
MSAL always appends openid/profile/offline_access to a client-credentials token request, and real Entra ID accepts that request. The emulator's strict token-count check rejected it outright, breaking every MSAL-based client-credentials client (Java, and likely others). Filter OIDC scopes (reusing the existing OIDC_SCOPES set from scopes.ts) before applying the exactly-one-resource-scope rule, so the rule now targets only non-OIDC scopes as originally intended. Refs cmaneu#28
Author
|
I just noticed there was a spec already in place for this. I'm closing this PR until the implementation matches the existing spec. |
The maintainer accepted issue cmaneu#28 and added specs/2026-08-30_28-client-credentials-additional-scopes.md, which is stricter/more complete than the original fix on this branch. Bring the implementation in line: - resolveClientCredentialScope: narrow the companion-scope allowlist to exactly openid/profile/offline_access (matching what Azure Identity/MSAL actually sends for this grant), rather than reusing scopes.ts's more lenient OIDC_SCOPES (which also admits email). email now correctly stays rejected as an unrecognized extra. - Reject a duplicated companion scope (e.g. 'openid openid ...'). - Add an effectiveScope to the resolved result; token.ts now uses it to normalize the response scope to the effective <resource>/.default only, never echoing OIDC companions the caller sent. - Extend unit + integration coverage for the above. - Add samples/java-client-credentials/: a real com.azure:azure-identity (MSAL4J) daemon reproducing the exact issue cmaneu#28 request shape against a live emulator, plus a CI smoke job. - memory/decisions.md: record the decision per project convention. Closes cmaneu#28
benrobot
added a commit
to benrobot/entra-local
that referenced
this pull request
Aug 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The client-credentials grant rejected any
scopecontaining more than one token, including theOIDC companion scopes (
openid,profile,offline_access) that MSAL/Azure Identity alwaysappends to a client-credentials request. Real Entra ID accepts this request. As shipped, this made
the emulator unusable with the MSAL client-credentials flow in any language.
This is the defect reported in #28. The maintainer has since added an accepted spec —
specs/2026-08-30_28-client-credentials-additional-scopes.md—which this PR implements in full.
What the official docs actually require
The client credentials flow docs
describe the
scoperequirement as being about the resource, not about excluding OIDC scopes:The scopes and permissions docs
spell out the actual restriction on mixing
.defaultwith other scopes, and it's about resourcescopes, not OIDC scopes:
That example combines
.defaultwith another resource-scoped permission (Mail.Read), notwith
openid/profile/offline_access. In practice OIDC companion scopes can't be rejected here,since Azure Identity/MSAL (Java, .NET, Node, Python) always appends them for this grant — if real
Entra ID rejected that, no MSAL-based daemon app could authenticate.
Fix (per the accepted spec)
resolveClientCredentialScope(src/identity/clientCredentials.ts):<resource>/.defaultscope plus zero or more of a narrowcompanion-scope allowlist —
openid,profile,offline_access— matching exactly what AzureIdentity sends for this grant. This is deliberately not the more lenient
OIDC_SCOPESsetalready used by
/authorize//devicecode(which also admitsemail):emailisn't one of thescopes MSAL appends here, so it's still rejected as an unrecognized extra, per the spec's
contract table.
openid openid <default>).effectiveScopeon the resolved result.handleClientCredentials(token.ts) nowuses it to set the response
scope, so a successful response always normalizes to the effective<resource>/.defaultonly — it never echoes back the OIDC companion scopes the caller sent(they weren't granted as delegated permissions in this app-only flow).
Errors preserved/added per the spec's contract table:
invalid_requestinvalid_scopeemail,User.Read, or two.defaults) →invalid_scopeinvalid_scope<resource>/.default→invalid_scope(unchanged)Java Azure Identity sample (
samples/java-client-credentials/)A standalone Maven sample using the official
com.azure:azure-identity(MSAL4J) library'sClientSecretCredential— the exact library/scenario reported in #28:https://graph.microsoft.com/.default; Azure Identity appends the OIDC companionscopes itself (the sample never adds them).
aud,appid/azp,roles, noscp/user claims) and callsGET /graph/v1.0/users.--smokemode for CI (this flow has no human interaction to begin with, so it's the same flow,just a single machine-readable status line).
Trust.java): trusts the emulator's owndata/tls/cert.pemwhen present,else falls back to trust-all with a loud warning — never for anything but a local emulator.
request with
openid profile offline_access https://graph.microsoft.com/.defaultsucceeds,decodes to
aud=https://graph.microsoft.com,roles=[], noid_token/refresh_token, andGET /graph/v1.0/usersreturns 200.samples/README.mdindex entry and ajava-client-credentials-sampleCI job (JDK 17 +Maven, mirrors the existing
node-cli-samplejob) that builds/starts the emulator and assertsthe smoke output.
Tests
test/unit/client-credentials-helpers.test.ts/test/integration/client-credentials.test.ts:added cases for the full companion-scope allowlist (any order),
emailrejection, duplicatecompanion-scope rejection, and response-scope normalization; existing cases updated to match.
memory/decisions.md: recorded the decision per the repo's append-only convention.lint,typecheck,build,test(412 passed / 6 skipped),test:e2e(including the realmsal-nodeclient-credentials e2e).Closes #28