Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion auth/api/iam/generated.go

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

37 changes: 29 additions & 8 deletions auth/api/iam/openid4vci.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/crypto"
nutsHttp "github.com/nuts-foundation/nuts-node/http"
"github.com/nuts-foundation/nuts-node/vdr/didweb"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
)

Expand All @@ -45,14 +46,34 @@ func (r Wrapper) RequestOpenid4VCICredentialIssuance(ctx context.Context, reques
// why did oapi-codegen generate a pointer for the body??
return nil, core.InvalidInputError("missing request body")
}
walletDID, err := did.ParseDID(request.Body.WalletDid)
if err != nil {
return nil, core.InvalidInputError("invalid wallet DID")
}
if owned, err := r.subjectOwns(ctx, request.SubjectID, *walletDID); err != nil {
return nil, err
} else if !owned {
return nil, core.InvalidInputError("wallet DID does not belong to the subject")
var walletDID *did.DID
var err error
if request.Body.WalletDid != nil {
walletDID, err = did.ParseDID(*request.Body.WalletDid)
if err != nil {
return nil, core.InvalidInputError("invalid wallet DID")
}
if owned, err := r.subjectOwns(ctx, request.SubjectID, *walletDID); err != nil {
return nil, err
} else if !owned {
return nil, core.InvalidInputError("wallet DID does not belong to the subject")
}
} else {
// wallet_did omitted: default to the subject's sole did:web DID.
dids, err := r.subjectManager.ListDIDs(ctx, request.SubjectID)
if err != nil {
return nil, err
}
var webDIDs []did.DID
for _, curr := range dids {
if curr.Method == didweb.MethodName {
webDIDs = append(webDIDs, curr)
}
}
if len(webDIDs) != 1 {
return nil, core.InvalidInputError("wallet_did is required: subject does not have exactly one did:web DID (found %d)", len(webDIDs))
}
walletDID = &webDIDs[0]
}
// Parse the issuer
issuer := request.Body.Issuer
Expand Down
44 changes: 42 additions & 2 deletions auth/api/iam/openid4vci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

"github.com/nuts-foundation/nuts-node/core/to"

"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/auth/openid4vci"
"github.com/nuts-foundation/nuts-node/crypto"
Expand Down Expand Up @@ -60,7 +61,7 @@ func TestWrapper_RequestOpenid4VCICredentialIssuance(t *testing.T) {
AuthorizationDetails: []AuthorizationDetail{{Type: "openid_credential", CredentialConfigurationId: "UniversityDegreeCredential", Format: to.Ptr("vc+sd-jwt")}},
Issuer: issuerClientID,
RedirectUri: redirectURI,
WalletDid: holderDID.String(),
WalletDid: to.Ptr(holderDID.String()),
},
})
require.NoError(t, err)
Expand All @@ -77,6 +78,45 @@ func TestWrapper_RequestOpenid4VCICredentialIssuance(t *testing.T) {
assert.Equal(t, "code", redirectUri.Query().Get("response_type"))
assert.Equal(t, `[{"credential_configuration_id":"UniversityDegreeCredential","format":"vc+sd-jwt","type":"openid_credential"}]`, redirectUri.Query().Get("authorization_details"))
})
t.Run("ok - wallet_did omitted, defaults to subject's sole did:web DID", func(t *testing.T) {
ctx := newTestClient(t)
ctx.openid4vciClient.EXPECT().OpenIDCredentialIssuerMetadata(nil, issuerClientID).Return(&metadata, nil)
ctx.iamClient.EXPECT().AuthorizationServerMetadata(nil, authServer).Return(&authzMetadata, nil)
req := requestCredentials(holderSubjectID, issuerClientID, redirectURI)
req.Body.WalletDid = nil

response, err := ctx.client.RequestOpenid4VCICredentialIssuance(nil, req)

require.NoError(t, err)
redirectUri, _ := url.Parse(response.(RequestOpenid4VCICredentialIssuance200JSONResponse).RedirectURI)
var stored OAuthSession
require.NoError(t, ctx.client.oauthClientStateStore().Get(redirectUri.Query().Get("state"), &stored))
assert.Equal(t, holderDID, *stored.OwnDID)
})
t.Run("error - wallet_did omitted, subject has multiple did:web DIDs", func(t *testing.T) {
subjectID := "multi-web"
otherWebDID := did.MustParseDID("did:web:example.com:iam:other")
ctx := newTestClient(t)
ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), subjectID).Return([]did.DID{holderDID, otherWebDID}, nil)
req := requestCredentials(subjectID, issuerClientID, redirectURI)
req.Body.WalletDid = nil

_, err := ctx.client.RequestOpenid4VCICredentialIssuance(nil, req)

assert.ErrorContains(t, err, "wallet_did is required")
})
t.Run("error - wallet_did omitted, subject has no did:web DIDs", func(t *testing.T) {
subjectID := "no-web"
natsDID := did.MustParseDID("did:nuts:12345")
ctx := newTestClient(t)
ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), subjectID).Return([]did.DID{natsDID}, nil)
req := requestCredentials(subjectID, issuerClientID, redirectURI)
req.Body.WalletDid = nil

_, err := ctx.client.RequestOpenid4VCICredentialIssuance(nil, req)

assert.ErrorContains(t, err, "wallet_did is required")
})
t.Run("ok - authorization_request_params merged into authorization request", func(t *testing.T) {
ctx := newTestClient(t)
ctx.openid4vciClient.EXPECT().OpenIDCredentialIssuerMetadata(nil, issuerClientID).Return(&metadata, nil)
Expand Down Expand Up @@ -234,7 +274,7 @@ func requestCredentials(subjectID string, issuer string, redirectURI string) Req
AuthorizationDetails: []AuthorizationDetail{{Type: "openid_credential", CredentialConfigurationId: "UniversityDegreeCredential"}},
Issuer: issuer,
RedirectUri: redirectURI,
WalletDid: holderDID.String(),
WalletDid: to.Ptr(holderDID.String()),
},
}
}
Expand Down
6 changes: 4 additions & 2 deletions docs/_static/auth/v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,13 @@ paths:
- issuer
- authorization_details
- redirect_uri
- wallet_did
properties:
wallet_did:
type: string
description: The DID to which the Verifiable Credential must be issued. Must be owned by the given subject.
description: |
The DID to which the Verifiable Credential must be issued. Must be owned by the given subject.
If omitted, defaults to the subject's sole did:web DID. Omitting it fails with a 400 if the
subject has zero or multiple did:web DIDs; in that case, wallet_did must be specified explicitly.
example: did:web:example.com
issuer:
type: string
Expand Down
1 change: 1 addition & 0 deletions docs/pages/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Unreleased
* #4078: Add the experimental RFC 7523 ``jwt-bearer`` two-VP token request flow, gated behind ``auth.experimental.jwtbearerclient`` (default ``false``, subject to change) by @stevenvegt in https://github.com/nuts-foundation/nuts-node/pull/4227
* #4078: Expose the experimental two-VP flow on ``POST /internal/auth/v2/{subjectID}/request-service-access-token`` via the optional ``service_provider_subject_id`` body field by @stevenvegt in https://github.com/nuts-foundation/nuts-node/pull/4228
* #4233: ``request-credential`` API gains an optional ``credential_request_params`` JSON object overlaid on top of the OpenID4VCI Credential Request body sent to the issuer. Lets the wallet talk to issuers that accept additional fields, or to override the credential request entirely.
* #4365: ``request-credential`` API's ``wallet_did`` is now optional. When omitted, it defaults to the subject's sole ``did:web`` DID; the call fails with a 400 if the subject has zero or multiple ``did:web`` DIDs.

****************
Peanut (v6.2.8)
Expand Down
4 changes: 3 additions & 1 deletion e2e-tests/browser/client/iam/generated.go

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

Loading