Skip to content

Client-side RFC 7523 JWT Bearer grant with two VPs (PSA 10.10) #4078

@stevenvegt

Description

@stevenvegt

Related: #3965, #4038

Problem Statement

The Nuts node currently uses the vp_token-bearer grant type (RFC021) with a single Verifiable Presentation in access token requests. The LSPxNuts PSA section 10.10 OAuth profile requires the RFC 7523 jwt-bearer grant type (PSA 10.10.1) with two separate VPs in the token request (PSA 10.10.6): VP1 from the healthcare provider (zorginstelling) as the authorization grant, and VP2 from the service provider (dienstverlener) as the client assertion. This separation is needed because the party making the technical request (the service provider) is not the same party that holds the authorization (the healthcare provider). The node must support both flows simultaneously to maintain backward compatibility with existing use cases.

Solution

Extend the client-side access token request flow to support the RFC 7523 jwt-bearer grant type with two VPs. When the remote AS advertises jwt-bearer support in its metadata (PSA 10.10.4), the node builds two VPs from two separate wallets: VP1 from the healthcare provider's wallet (identified by the subjectID in the API request) and VP2 from the service provider's wallet (identified by a configured SP DID). The flow auto-negotiates: if the AS doesn't support jwt-bearer, the node falls back to the existing vp_token-bearer flow with a single VP.

User Stories

  1. As an EHR developer, I want to request access tokens from authorization servers that require the RFC 7523 JWT bearer grant type, so that I can integrate with the GF infrastructure.
  2. As an EHR developer, I want the node to automatically select the correct grant type based on the remote AS's capabilities, so that I don't need to know which flow the AS requires.
  3. As an EHR developer, I want the existing request-service-access-token API to remain unchanged, so that my integration doesn't break when the node adds JWT bearer support.
  4. As a Nuts node operator, I want to configure the service provider DID once globally, so that all token requests use the correct SP identity for client assertions.
  5. As a Nuts node operator, I want to configure a client presentation definition per credential profile, so that the node knows which SP credentials to include in VP2.
  6. As a use-case designer, I want the service provider and healthcare provider to be authenticated separately in the token request, so that the delegation model is correctly represented.
  7. As a Nuts node operator, I want the node to sign VP2 with the service provider's keys and VP1 with the healthcare provider's keys, so that each party's identity is cryptographically asserted.
  8. As an EHR developer, I want clear error messages when the SP wallet lacks the required credentials for VP2, so that I can diagnose configuration issues.
  9. As an EHR developer, I want the two-layer scope model — use-case scope and resource scopes (PSA 10.10.2) — to work with the JWT bearer flow, so that SMART on FHIR scopes are forwarded alongside the credential profile.

Implementation Decisions

Auto-negotiation

When RequestRFC021AccessToken fetches the remote AS's metadata, it checks grant_types_supported. If urn:ietf:params:oauth:grant-type:jwt-bearer is present and a client PD is configured for the requested credential profile, the node uses the two-VP flow. Otherwise it falls back to vp_token-bearer with a single VP. This builds on the pattern from the existing PoC commit (19f5960).

Two VP construction

The existing generic VP builder (BuildSubmission) is called twice:

  • VP1 (assertion): Built from the healthcare provider's wallet (identified by subjectID), using the organization PD from local policy config. This is the authorization grant — it contains credentials such as HealthcareProviderCredential, PatientEnrollmentCredential, etc.
  • VP2 (client_assertion): Built from the service provider's wallet (identified by the configured SP DID), using a new client PD from local policy config. This is the client assertion — it contains credentials such as ServiceProviderCredential, ServiceProviderDelegationCredential.
  • client_assertion_type is set to urn:ietf:params:oauth:client-assertion-type:jwt-bearer per RFC 7523.

SP identity configuration

The existing network.nodedid configuration identifies the vendor/SP operating the node. This config field is deprecated in its current location and should be renamed/relocated to a more appropriate place (e.g., a top-level or auth-level serviceprovider.did config). The old config key should remain supported with a deprecation warning for backward compatibility.

The node must have signing keys for the SP DID and credentials in its wallet matching the client PD.

Policy config extension

The policy configuration files gain a client wallet owner type alongside the existing organization and user:

{
  "medication-overview": {
    "organization": { /* PD for VP1 — healthcare provider credentials */ },
    "client": { /* PD for VP2 — service provider credentials */ },
    "user": { /* PD for user identity — optional */ }
  }
}

When the client PD is absent for a credential profile, VP2 is not built, and the node falls back to the single-VP flow regardless of AS metadata.

Token request parameters (jwt-bearer flow, PSA 10.10.6)

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
assertion=<VP1 JWT>
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion=<VP2 JWT>
presentation_submission=<JSON mapping VP1 to PD>
scope=<use-case scope + resource scopes>

Dependencies

Modules to build/modify

  1. Grant type negotiation and two-VP flow (modify existing RFC021 client): extend to build two VPs by calling the existing BuildSubmission twice (once for HCP wallet with organization PD, once for SP wallet with client PD) when jwt-bearer is negotiated. Construct the token request with both assertion and client_assertion parameters.
  2. Policy config loader (modify): support client wallet owner type in policy configuration files.
  3. SP DID configuration (modify): rename/relocate network.nodedid to a dedicated SP config field. Maintain backward compatibility with deprecation warning.

Testing Decisions

A good test verifies that given a specific AS metadata response, wallet state, and policy configuration, the correct token request is built (single VP or two VPs, correct grant type, correct parameters).

Modules to test:

  • Two-VP flow: unit tests verifying that VP1 is built from HCP wallet and VP2 from SP wallet, each using the correct PD
  • Grant type negotiation: unit tests for auto-selection based on AS metadata, fallback when AS doesn't support jwt-bearer, fallback when client PD is absent
  • Token request construction: unit tests verifying correct parameters for both jwt-bearer and vp_token-bearer flows
  • Policy config: unit tests for loading client PD alongside organization and user
  • Prior art: existing tests in auth/client/iam/openid4vp_test.go (including the PoC test for jwt-bearer), policy/local_test.go

Out of Scope

  • Server-side JWT bearer support (covered in a separate PRD)
  • Delegation verification by client (PSA 10.10.5)
  • DPoP changes (already implemented, PSA 10.10.6)
  • URA-based token endpoint addressing (PSA 10.10.3)
  • VC-type-specific validation logic
  • Credential validation beyond wallet selection (expiration/revocation checks during selection are existing behavior)
  • AuthZen callback (server-side concern, see Idea: decouple required credentials from OAuth2 scope #4038)
  • Multiple SP identities per node
  • Custom error codes (PSA 10.10.10, covered in server-side PRD)

Further Notes

  • The client PD is optional per credential profile. If absent, the node assumes the remote AS doesn't require client credentials in a separate VP.
  • Long-term, a standardized mechanism for the AS to advertise per-wallet-owner PDs (e.g., in metadata or via the presentation definition endpoint) could replace the local client PD config. No standard exists for this currently.
  • This PRD references the LSPxNuts PSA section 10.10 OAuth profile. The spec is approximately 80% finalized; some details may change.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions