Server-side multi-scope flow & scope policy evaluation#4179
Open
stevenvegt wants to merge 10 commits into4144-2-authzen-clientfrom
Open
Server-side multi-scope flow & scope policy evaluation#4179stevenvegt wants to merge 10 commits into4144-2-authzen-clientfrom
stevenvegt wants to merge 10 commits into4144-2-authzen-clientfrom
Conversation
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (4)
🤖 Increase coverage with AI coding...🚦 See full report on Qlty Cloud » 🛟 Help
|
7c44b2a to
4f08bcf
Compare
20 tasks
Returns the full CredentialProfileMatch instead of only WalletOwnerMapping. Callers that only need WalletOwnerMapping access match.WalletOwnerMapping. Prepares for scope policy enforcement on the server side. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Profile-only scope policy rejects token requests with extra scopes beyond the credential profile scope. Check happens early, before expensive VP signature verification. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies that passthrough scope policy grants all requested scopes. No implementation change needed — existing code already passes the full scope string through when not rejected by profile-only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the implicit pass-through of the raw input scope with an explicit grantedScopesForPolicy switch. Profile-only grants only the credential profile scope. Passthrough grants the profile scope plus other scopes. Dynamic returns an error (not yet implemented). Prevents accidental scope pass-through when a new policy is added. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LocalPDP creates an authzen.Client during Configure when an AuthZen endpoint is configured. PDPBackend exposes it via AuthZenEvaluator(), returning nil when no endpoint is set. This keeps AuthZen client ownership in the policy module (which owns the config) and avoids wiring through cmd/root.go before config is loaded. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When scope_policy is 'dynamic', the server builds an AuthZen batch evaluation request from the validated credentials (claims extracted via resolveInputDescriptorValues, matching introspection behavior) and calls the PDP. The credential profile scope must be approved by the PDP or the request is denied. Other scopes are granted only when the PDP approves them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rror - Partial denial: denied other scopes excluded, approved ones granted - PDP denies credential profile scope: request rejected (access_denied) - PDP call fails: server_error returned with details Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use StrictHTTPClient (timeout + response body limit) for AuthZen client instead of http.DefaultClient (memory #4185) - Wrap credentialMap() / resolveInputDescriptorValues errors as OAuth2Error to preserve the spec-compliant error response contract - Use generic Description for PDP errors, keep details in InternalError to avoid leaking PDP internals to the OAuth2 client - Tighten dynamic-approves-all test to verify AuthZen request shape (subject.type, action.name, context.policy, evaluations layout) - Fix AuthZenEvaluator interface doc comment - Apply gofmt Follow-up issues: - #4202: apply scope policy to OpenID4VP / auth-code flow - Claim role-bucket mismatch deferred to #4080 (two-VP flow) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
485a64b to
de5213b
Compare
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.

Parent PRD
#4144
Summary
Enforces scope policy on the server side of the S2S (RFC021) token flow. Rejects extra scopes in profile-only mode, forwards all in passthrough, and calls an AuthZen PDP for dynamic evaluation. The access token's
scopefield reflects the granted scopes, never the raw request.What changed
Server-side token handler (
auth/api/iam/s2s_vptoken.go):grantedScopesForPolicyswitch derives granted scopes explicitly per policy — never passes the raw input throughevaluateDynamicScopesbuilds an AuthZenEvaluationsRequest, calls the PDP, and maps per-scope decisions to the granted setaccess_denied; other denials simply excluded; PDP error → genericserver_error(details inInternalError, not inDescription)Policy interface (
policy/interface.go,policy/local.go):AuthZenEvaluatorinterface in thepolicypackage (Evaluate(ctx, req) (map[string]bool, error))PDPBackendgets anAuthZenEvaluator()method returning the evaluator ornilLocalPDP.Configurecreates anauthzen.Client(viaStrictHTTPClient— 1MB body limit, 10s timeout, TLS enforced) when an endpoint is configuredHelper rename (
auth/api/iam/validation.go):presentationDefinitionForScope→findCredentialProfile, now returns the full*CredentialProfileMatchs2s_vptoken.goandopenid4vp.goupdated to usematch.WalletOwnerMappingwhere they only needed the PDsClaim extraction: reuses the existing
resolveInputDescriptorValues(same pattern as introspection) to buildSubject.Properties.Organization— no new helper needed.Mock regenerated:
policy.MockPDPBackendnow hasAuthZenEvaluator(); newpolicy.MockAuthZenEvaluatoradded.How to review
Start with
s2s_vptoken.go— the policy-dispatch is the core of this PR:handleS2SAccessTokenRequest— placement of the profile-only early reject (before VP verification) and the call tograntedScopesForPolicyafter verificationgrantedScopesForPolicy— the switch statement, easy to followevaluateDynamicScopes— AuthZen request construction, error paths (nil evaluator, PDP error, profile scope denial)Then
policy/interface.go+policy/local.go— the evaluator wiring:AuthZenEvaluatorinterface is one-method, kept on the policy side so callers don't importpolicy/authzendirectly for the typeLocalPDP.Configurecreates the client when endpoint is configured — noteStrictHTTPClientusage for body limit / timeoutTests (
s2s_vptoken_test.go):DoAndReturn— catches regressions in request constructionDeviations from spec
ExtractSubjectPropertieshelper. Instead reusedresolveInputDescriptorValues— same extraction as introspection, no new helper.PDPBackend.AuthZenEvaluator()— keeps ownership with the module that owns the config and avoids dependency-order issues incmd/root.go(config isn't loaded whenRegisterRoutesruns).presentationDefinitionForScopetofindCredentialProfile— the return value now is the full match, not just a mapping.Known follow-ups
resolveInputDescriptorValuesmerges claims across wallet owner types (organization + user) into a single map, dumped intoSubject.Properties.Organization. Correct for today's single-VP / organization-only flow. The two-VP flow in Server-side RFC 7523 JWT Bearer grant with two VPs (PSA 10.10) #4080 will need to split claims by role.Dependencies
Depends on:
Review order: Review #4176 and #4177 first; this PR builds directly on both.
Design context
StrictHTTPClientis used for the AuthZen client.Acceptance Criteria
scopefield contains only granted scopesPDPBackend.AuthZenEvaluator()(nil when not configured)Original implementation spec (used during AI-assisted development)
Parent PRD
#4144
Implementation Spec
Overview
Modify the server-side token request flow to support mixed OAuth2 scopes with scope policy evaluation. The server parses the incoming scope string, validates the VP against the credential profile scope's PD, then applies the scope policy to determine which scopes are granted.
Key files modified
auth/api/iam/validation.go— RenamedpresentationDefinitionForScope→findCredentialProfile, returns fullCredentialProfileMatchauth/api/iam/s2s_vptoken.go— Scope policy enforcement, dynamic PDP evaluation via AuthZenauth/api/iam/openid4vp.go— Updated caller to use new helperauth/api/iam/api.go— No Wrapper changes (evaluator accessed viapolicyBackend.AuthZenEvaluator())policy/interface.go— AddedAuthZenEvaluatorinterface +AuthZenEvaluator()method onPDPBackendpolicy/local.go— Createsauthzen.ClientduringConfigurewhen endpoint is set (usingStrictHTTPClient)Design decisions
policymodule owns the config and creates the AuthZen client duringConfigure()(when config is loaded).PDPBackendexposes it viaAuthZenEvaluator(). This avoids wiring throughcmd/root.gobefore config is available.grantedScopesForPolicyexplicitly computes granted scopes per policy — no implicit pass-through of raw input. Prevents silent scope leakage when new policies are added.resolveInputDescriptorValues: Same extraction pattern as introspection.InputDescriptorConstraintIdMapis reused as the AuthZenSubject.Properties.Organizationpayload.http/client.New(timeout)— enforces TLS, bounds response body size to 1MB, applies 10s timeout (memory #4185).Deviations from original spec
ExtractSubjectPropertieshelper. Instead we reuseresolveInputDescriptorValueswhich already does the extraction for introspection. Single pattern, no new helper.PDPBackend.AuthZenEvaluator()keeps ownership with the module that owns the config and avoids dependency order issues incmd/root.go.Known follow-ups
resolveInputDescriptorValuesmerges claims across wallet owner types (organization + user). Currently dumped intoSubject.Properties.Organization. This is correct for today's single-VP flow (organization-only). The two-VP flow in Server-side RFC 7523 JWT Bearer grant with two VPs (PSA 10.10) #4080 will need to split claims by role.Acceptance Criteria
scopefield contains only granted scopesPDPBackend.AuthZenEvaluator()(evaluates to nil when not configured)