Skip to content

fix(authserver): restrict each OAuth grant to its own kind of scope (#251) - #258

Open
dfcoffin wants to merge 3 commits into
mainfrom
fix/251-per-grant-scope-policy
Open

dfcoffin wants to merge 3 commits into
mainfrom
fix/251-per-grant-scope-policy

Conversation

@dfcoffin

@dfcoffin dfcoffin commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Part of #251. Each OAuth grant may now yield only its own kind of scope, even for a client registered for both kinds. Spring Authorization Server checks only that a requested scope is registered for the client, whatever the grant. Without this rule, a Third Party registered for client_credentials and a customer Function Block scope could mint a customer-data token with no customer's consent.

Both rules are allowlists that fail closed:

Grant May yield only Otherwise
client_credentials (client access token, renewal, infrastructure clients) the exact strings ThirdParty_Admin_Access, DataCustodian_Admin_Access, Upload_Admin_Access, FB=36_40, authserver.admin, client.create 400 invalid_scope
authorization_code (customer consent) openid, profile, or a scope that parses to Function Blocks, none of them 36 or 40 invalid_scope redirected to the client's redirect URI (same as Spring's own scope error)

The authorization-code check parses scopes the way the Data Custodian's EspiScope does (;-separated, trimmed, case-insensitive FB key, _-separated integers), so a different spelling can't slip a customer scope past the token endpoint. An unparseable scope is rejected. Refresh tokens can only narrow the scopes of their original grant, so they need no rule.

Changes

  • New grant/EspiGrantScopePolicy: validators that run after Spring's defaults (OAuth2ClientCredentialsAuthenticationValidator.DEFAULT_SCOPE_VALIDATOR, OAuth2AuthorizationCodeRequestAuthenticationValidator), so the existing checks are kept.
  • AuthorizationServerConfig: installs them on the token and authorization endpoints' providers.

Not in this PR (rest of #251)

Registration granting client_credentials to Third Parties with server-assigned ThirdParty_Admin_Access / FB=36_40, the registration access token, and the DC accepting it for ApplicationInformation. This rule is the safety net that makes granting client_credentials at registration safe.

Test plan

  • EspiGrantScopePolicyTest (27):
    • each rule, with Spring's default check still applied;
    • alternative spellings (IntervalDuration=3600;FB=4_5_15, fb=4_5_15, FB =4) rejected for client_credentials;
    • reserved FBs (FB=36, fb=36_40, FB=4_36), administrative, unparseable (FB=abc) and unknown scopes rejected for authorization_code;
    • the invalid_scope redirect keeps redirect_uri and state.
  • All non-Docker AS tests pass locally
  • SecurityIntegrationTest "ESPI per-grant scope policy (Registration access token: issue it on RFC 7591 registration, renew via FB=36_40, and require it for ApplicationInformation #251)" (Docker, CI): a client registered for both grants gets invalid_scope for a customer scope via client_credentials and for ThirdParty_Admin_Access via /oauth2/authorize, and succeeds for the scope that matches the grant

🤖 Generated with Claude Code

dfcoffin and others added 2 commits November 14, 2026 18:37
…251)

Spring Authorization Server only checks that a requested scope is
registered for the client, whatever the grant. ESPI keeps customer data
and administrative tokens apart, so:

- client_credentials cannot be granted an ESPI customer data scope
  (any FB=... other than the FB=36_40 registration-token renewal
  scope);
- authorization_code cannot be granted an administrative scope
  (ThirdParty_Admin_Access, DataCustodian_Admin_Access, FB=36_40,
  authserver.admin, client.create). The client receives invalid_scope
  at its redirect URI, as Spring does for its own scope check.

EspiGrantScopePolicy runs after Spring's default validators on the token
and authorization endpoints. Tested with unit tests and a Docker-backed
SecurityIntegrationTest case using a client registered for both grants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Data Custodian parses scopes tolerantly (any segment order, trimmed,
case-insensitive FB key), so a prefix check on "FB=" could be bypassed
with a different spelling, and Upload_Admin_Access was missing from the
administrative list. Both rules now fail closed:

- client_credentials: only the exact administrative scope strings,
  including Upload_Admin_Access;
- authorization_code: only openid/profile, or a scope that parses like
  EspiScope to Function Blocks other than the reserved 36 and 40.
  Unparseable or unknown scopes are rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… introspection test

The GHSA-mh65 test minted a client_credentials token for the placeholder
scope FB_1, which the per-grant allowlist now rejects. It requests
ThirdParty_Admin_Access instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant