Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
81818a3
Initialize branch for PR
stevenvegt May 1, 2026
8c73791
Load client PD block from policy config
stevenvegt May 1, 2026
9730996
Guard schema validation of the client PD block
stevenvegt May 1, 2026
7f5d69e
Allow client-only credential profiles in policy config
stevenvegt May 1, 2026
f1c1054
Self-review fixes for client PD support
stevenvegt May 1, 2026
ab3c606
Rename client wallet-owner type to service_provider
stevenvegt May 1, 2026
156ed6d
Reword service_provider doc to state when it applies, not when it doe…
stevenvegt May 1, 2026
fe93b2e
Initialize branch for PR
stevenvegt May 1, 2026
7df21b3
Plumb serviceProviderSubjectID and the jwt-bearer feature flag
stevenvegt May 1, 2026
cfc6d48
Gate the two-VP flow behind the experimental flag
stevenvegt May 1, 2026
ef61c0f
Reject two-VP requests when AS does not advertise jwt-bearer
stevenvegt May 1, 2026
df974c9
Reject two-VP requests when no service_provider PD is configured
stevenvegt May 1, 2026
ea4bbc3
Build the jwt-bearer two-VP token request
stevenvegt May 1, 2026
6fe03d5
Capture VP1 field-id values into VP2 credential_selection
stevenvegt May 1, 2026
2ede2b6
Add PresentationSubmission.ResolveVP for single-VP callers
stevenvegt May 2, 2026
a0d28ea
Tighten the two-VP path: ResolveVP, params.DIDMethods, no client_id
stevenvegt May 2, 2026
ad8cff6
Extract requestVPTokenAccessToken so RequestRFC021AccessToken dispatches
stevenvegt May 2, 2026
7d622df
Rename RequestRFC021AccessToken to RequestServiceAccessToken
stevenvegt May 2, 2026
5023f0c
Extract loadAndValidateProfile and unit-test the rules directly
stevenvegt May 2, 2026
b2ba045
Fold the single-VP build into buildSubmissionForSubject
stevenvegt May 2, 2026
d7ed7b4
Populate Envelope.raw in ResolveVP
stevenvegt May 4, 2026
c68b7e9
Wire DPoP through the jwt-bearer two-VP flow
stevenvegt May 4, 2026
ae78489
End-to-end test for cross-VP field-id binding
stevenvegt May 4, 2026
b9d3c98
Return the resolved scope on the TokenResponse
stevenvegt May 4, 2026
c4b817e
Doc polish for the jwt-bearer two-VP flow
stevenvegt May 4, 2026
3f8def8
Move JwtBearerClientAssertionType into a dedicated const block
stevenvegt May 4, 2026
603d432
Regenerate server_options.rst
stevenvegt May 4, 2026
c5bd076
Wrap the jwt-bearer flag-flip in a tiny test helper
stevenvegt May 4, 2026
fd621f4
applyCapturedFieldsToSelection allocates a fresh selection map
stevenvegt May 4, 2026
584425e
Use typed OAuth2Errors for jwt-bearer rejections
stevenvegt May 4, 2026
c80a2ee
Replace PresentationSubmission.ResolveVP with pe.NewEnvelopeFromVP
stevenvegt May 4, 2026
4f592ca
Spell out the jwt-bearer URN in the AS-doesn't-advertise error
stevenvegt May 4, 2026
d88dc8f
Hoist the VP-assertion lifetime into a named constant
stevenvegt May 4, 2026
262aade
Generate a fresh nonce for VP2 in the jwt-bearer flow
stevenvegt May 4, 2026
410e67a
Surface the JSONPath in cross-VP binding errors
stevenvegt May 4, 2026
7348a9c
Rename vp1/vp2 locals to organizationVP/serviceProviderVP
stevenvegt May 4, 2026
e898ff3
Rename Client.RequestServiceAccessToken's subjectDID parameter to sub…
stevenvegt May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,7 @@ func (r Wrapper) RequestServiceAccessToken(ctx context.Context, request RequestS
}

clientID := r.subjectToBaseURL(request.SubjectID)
tokenResult, err := r.auth.IAMClient().RequestRFC021AccessToken(ctx, clientID.String(), request.SubjectID, request.Body.AuthorizationServer, request.Body.Scope, useDPoP, credentials, credentialSelection)
tokenResult, err := r.auth.IAMClient().RequestServiceAccessToken(ctx, clientID.String(), request.SubjectID, request.Body.AuthorizationServer, request.Body.Scope, useDPoP, credentials, credentialSelection, nil)
if err != nil {
// this can be an internal server error, a 400 oauth error or a 412 precondition failed if the wallet does not contain the required credentials
return nil, err
Expand Down
18 changes: 9 additions & 9 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}
request.Params.CacheControl = to.Ptr("no-cache")
// Initial call to populate cache
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil).Return(response, nil).Times(2)
ctx.iamClient.EXPECT().RequestServiceAccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil, nil).Return(response, nil).Times(2)
token, err := ctx.client.RequestServiceAccessToken(nil, request)

// Test call to check cache is bypassed
Expand All @@ -907,7 +907,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
TokenType: "Bearer",
ExpiresIn: to.Ptr(900),
}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil).Return(response, nil)
ctx.iamClient.EXPECT().RequestServiceAccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil, nil).Return(response, nil)

token, err := ctx.client.RequestServiceAccessToken(nil, request)

Expand Down Expand Up @@ -946,7 +946,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
t.Run("cache expired", func(t *testing.T) {
cacheKey := accessTokenRequestCacheKey(request)
_ = ctx.client.accessTokenCache().Delete(cacheKey)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil).Return(&oauth.TokenResponse{AccessToken: "other"}, nil)
ctx.iamClient.EXPECT().RequestServiceAccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil, nil).Return(&oauth.TokenResponse{AccessToken: "other"}, nil)

otherToken, err := ctx.client.RequestServiceAccessToken(nil, request)

Expand All @@ -963,7 +963,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
Scope: "first second",
TokenType: &tokenTypeBearer,
}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", false, nil, nil).Return(&oauth.TokenResponse{}, nil)
ctx.iamClient.EXPECT().RequestServiceAccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", false, nil, nil, nil).Return(&oauth.TokenResponse{}, nil)

_, err := ctx.client.RequestServiceAccessToken(nil, RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body})

Expand All @@ -972,7 +972,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
t.Run("ok with expired cache by ttl", func(t *testing.T) {
ctx := newTestClient(t)
request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil).Return(&oauth.TokenResponse{ExpiresIn: to.Ptr(5)}, nil)
ctx.iamClient.EXPECT().RequestServiceAccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil, nil).Return(&oauth.TokenResponse{ExpiresIn: to.Ptr(5)}, nil)

_, err := ctx.client.RequestServiceAccessToken(nil, request)

Expand All @@ -981,7 +981,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
})
t.Run("error - no matching credentials", func(t *testing.T) {
ctx := newTestClient(t)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil).Return(nil, pe.ErrNoCredentials)
ctx.iamClient.EXPECT().RequestServiceAccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil, nil).Return(nil, pe.ErrNoCredentials)

_, err := ctx.client.RequestServiceAccessToken(nil, RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body})

Expand All @@ -997,8 +997,8 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
ctx.client.storageEngine = mockStorage

request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil).Return(&oauth.TokenResponse{AccessToken: "first"}, nil)
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil).Return(&oauth.TokenResponse{AccessToken: "second"}, nil)
ctx.iamClient.EXPECT().RequestServiceAccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil, nil).Return(&oauth.TokenResponse{AccessToken: "first"}, nil)
ctx.iamClient.EXPECT().RequestServiceAccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, nil, nil, nil).Return(&oauth.TokenResponse{AccessToken: "second"}, nil)

token1, err := ctx.client.RequestServiceAccessToken(nil, request)
require.NoError(t, err)
Expand All @@ -1023,7 +1023,7 @@ func TestWrapper_RequestServiceAccessToken(t *testing.T) {
{ID: to.Ptr(ssi.MustParseURI("not empty"))},
}
request := RequestServiceAccessTokenRequestObject{SubjectID: holderSubjectID, Body: body}
ctx.iamClient.EXPECT().RequestRFC021AccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, *body.Credentials, nil).Return(response, nil)
ctx.iamClient.EXPECT().RequestServiceAccessToken(nil, holderClientID, holderSubjectID, verifierURL.String(), "first second", true, *body.Credentials, nil, nil).Return(response, nil)

_, err := ctx.client.RequestServiceAccessToken(nil, request)

Expand Down
2 changes: 1 addition & 1 deletion auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func (auth *Auth) RelyingParty() oauth.RelyingParty {

func (auth *Auth) IAMClient() iam.Client {
keyResolver := resolver.DIDKeyResolver{Resolver: auth.vdrInstance.Resolver()}
return iam.NewClient(auth.vcr.Wallet(), keyResolver, auth.subjectManager, auth.keyStore, auth.jsonldManager.DocumentLoader(), auth.policyBackend, auth.strictMode, auth.httpClientTimeout)
return iam.NewClient(auth.vcr.Wallet(), keyResolver, auth.subjectManager, auth.keyStore, auth.jsonldManager.DocumentLoader(), auth.policyBackend, auth.strictMode, auth.httpClientTimeout, auth.config.Experimental.JwtBearerClient)
}

// Configure the Auth struct by creating a validator and create an Irma server
Expand Down
14 changes: 10 additions & 4 deletions auth/client/iam/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,17 @@ type Client interface {
PostAuthorizationResponse(ctx context.Context, vp vc.VerifiablePresentation, presentationSubmission pe.PresentationSubmission, verifierResponseURI string, state string) (string, error)
// PresentationDefinition returns the presentation definition from the given endpoint.
PresentationDefinition(ctx context.Context, endpoint string) (*pe.PresentationDefinition, error)
// RequestRFC021AccessToken is called by the local EHR node to request an access token from a remote OAuth2 Authorization Server using Nuts RFC021.
// credentials are additional VCs to include alongside wallet-stored credentials.
// RequestServiceAccessToken is called by the local EHR node to request an access token from a remote OAuth2 Authorization Server.
// When serviceProviderSubjectID is nil, the request uses the Nuts RFC021 vp_token-bearer single-VP flow.
// When serviceProviderSubjectID is non-nil it identifies a service-provider Nuts subject and triggers the RFC 7523
// jwt-bearer two-VP flow; that flow is only honored when the experimental jwt-bearer client feature is enabled and
// the AS advertises jwt-bearer.
// credentials are additional VCs to include alongside wallet-stored credentials. In the two-VP flow they are
// offered to both wallets; each PD selects what matches its input descriptors. Signed VCs flow through unchanged;
// unsigned self-attested credentials are auto-issued per holder DID by AutoCorrectSelfAttestedCredential.
// credentialSelection maps PD field IDs to expected values to disambiguate when multiple credentials match an input descriptor.
RequestRFC021AccessToken(ctx context.Context, clientID string, subjectDID string, authServerURL string, scopes string, useDPoP bool,
credentials []vc.VerifiableCredential, credentialSelection map[string]string) (*oauth.TokenResponse, error)
RequestServiceAccessToken(ctx context.Context, clientID string, subjectID string, authServerURL string, scopes string, useDPoP bool,
credentials []vc.VerifiableCredential, credentialSelection map[string]string, serviceProviderSubjectID *string) (*oauth.TokenResponse, error)

// OpenIdCredentialIssuerMetadata returns the metadata of the remote credential issuer.
// oauthIssuer is the URL of the issuer as specified by RFC 8414 (OAuth 2.0 Authorization Server Metadata).
Expand Down
12 changes: 6 additions & 6 deletions auth/client/iam/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading