Skip to content

Client-side multi-scope token flow#4178

Draft
stevenvegt wants to merge 5 commits into4144-1-scope-parsing-and-configfrom
4144-3-client-side-flow
Draft

Client-side multi-scope token flow#4178
stevenvegt wants to merge 5 commits into4144-1-scope-parsing-and-configfrom
4144-3-client-side-flow

Conversation

@stevenvegt
Copy link
Copy Markdown
Member

@stevenvegt stevenvegt commented Apr 13, 2026

Parent PRD

#4144

Implementation Spec

Overview

Modify the client-side token request flow to support mixed OAuth2 scopes. Introduces a PresentationDefinitionResolver that abstracts the decision of whether to fetch a PD from the remote AS or fall back to local policy resolution. When using local resolution, scope policy is enforced.

Design decisions

  • PresentationDefinitionResolver abstraction: The client (RequestRFC021AccessToken) should not decide where the PD comes from. A resolver encapsulates the remote-vs-local decision and scope policy enforcement, keeping the client focused on the OAuth flow.
  • Remote PD endpoint → trust remote AS: When the remote AS metadata advertises a PD endpoint, the resolver fetches the PD from there and returns the full scope string. The remote server enforces scope policy (covered by PR Server-side multi-scope flow & scope policy evaluation #4179).
  • No remote PD endpoint → local fallback: When no PD endpoint exists, the resolver calls FindCredentialProfile locally to get the PD and scope classification. Scope policy is enforced locally: profile-only rejects extra scopes, passthrough/dynamic forward all scopes.
  • Dynamic same as passthrough on client side: The client does not call the AuthZen PDP — the server handles dynamic scope evaluation at token-grant time (PR Server-side multi-scope flow & scope policy evaluation #4179).
  • Resolver on OpenID4VPClient: The resolver is a struct dependency, wired through AuthNewClient. Policy backend passed through Auth to enable local PD fallback.

Deviations from original spec

  • Spec assumed presentationDefinitionForScope was the modification point: That helper is server-side only. The actual client-side flow goes through RequestRFC021AccessToken → remote PD endpoint. The resolver replaces the direct PD fetch.
  • Spec didn't account for remote-vs-local PD resolution: The client fetches PDs from the remote AS when possible, falling back to local only when no PD endpoint exists. This enables integration with external (non-Nuts) authorization servers.
  • Policy backend wired through Auth struct: Added policyBackend parameter to NewAuthInstance and iam.NewClient to propagate the policy backend to the resolver.

Modified flow

RequestRFC021AccessToken(ctx, ..., scope, ...)
    1. Fetch remote AS metadata
    2. resolved, err := c.pdResolver.Resolve(ctx, scope, metadata)
       Internally:
       a. If metadata has PD endpoint → fetch from remote, return full scope
       b. If no PD endpoint → FindCredentialProfile locally, enforce scope policy
    3. Build VP using resolved.PresentationDefinition
    4. Send token request with resolved.Scope

Acceptance Criteria

  • PresentationDefinitionResolver resolves PD from remote when PD endpoint exists
  • Resolver falls back to local PD via FindCredentialProfile when no remote PD endpoint
  • Local fallback: profile-only rejects extra scopes
  • Local fallback: passthrough and dynamic forward all scopes
  • Remote path: all scopes forwarded, scope policy not enforced locally
  • RequestRFC021AccessToken uses resolver instead of direct PD fetch
  • Single-scope requests work unchanged (backwards compatible)
  • Unit tests cover both resolver paths with all scope policies
  • Nil policy backend guard (added during self-review)
  • Profile-only returns canonical scope, not raw input (added during self-review)

@qltysh
Copy link
Copy Markdown

qltysh bot commented Apr 13, 2026

Qlty

Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (4)

RatingFile% DiffUncovered Line #s
Coverage rating: C Coverage rating: C
auth/auth.go100.0%
Coverage rating: A Coverage rating: A
cmd/root.go100.0%
Coverage rating: B Coverage rating: B
auth/client/iam/openid4vp.go20.0%69-87
New file Coverage rating: A
auth/client/iam/pd_resolver.go95.7%63-64
Total79.1%
🤖 Increase coverage with AI coding...

In the `4144-3-client-side-flow` branch, add test coverage for this new code:

- `auth/client/iam/openid4vp.go` -- Line 69-87
- `auth/client/iam/pd_resolver.go` -- Line 63-64

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

stevenvegt and others added 5 commits April 14, 2026 10:06
Introduces PresentationDefinitionResolver that abstracts PD resolution.
When the remote AS metadata advertises a PD endpoint, the PD is fetched
remotely and the full scope string is returned for the token request.
Local fallback path is stubbed for the next cycle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When no remote PD endpoint exists, the resolver calls FindCredentialProfile
locally. Profile-only rejects extra scopes, passthrough/dynamic forward all.
Tests cover both remote and local paths with all scope policies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace direct PD fetch in RequestRFC021AccessToken with the
PresentationDefinitionResolver. The resolver is a dependency on
OpenID4VPClient, wired through Auth → NewClient. The policy backend
is passed through Auth to enable local PD fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add nil guard on policyBackend in resolveLocal
- Return canonical credential profile scope for profile-only (not raw input)
- Add comment explaining dynamic treated same as passthrough on client side
- Add tests: nil policy backend, missing org PD, remote endpoint error
- Fix import grouping in test file

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@stevenvegt stevenvegt force-pushed the 4144-3-client-side-flow branch from f1ee8fb to 0b190e6 Compare April 14, 2026 16:00
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.

Support mixed OAuth2 scopes with configurable scope policy

1 participant