diff --git a/auth/api/iam/generated.go b/auth/api/iam/generated.go index 13e0eeeda..66a735c0b 100644 --- a/auth/api/iam/generated.go +++ b/auth/api/iam/generated.go @@ -283,7 +283,9 @@ type RequestOpenid4VCICredentialIssuanceJSONBody struct { RedirectUri string `json:"redirect_uri"` // WalletDid The DID to which the Verifiable Credential must be issued. Must be owned by the given subject. - WalletDid string `json:"wallet_did"` + // 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. + WalletDid *string `json:"wallet_did,omitempty"` } // RequestServiceAccessTokenParams defines parameters for RequestServiceAccessToken. diff --git a/auth/api/iam/openid4vci.go b/auth/api/iam/openid4vci.go index c5817da2c..edcfe37e9 100644 --- a/auth/api/iam/openid4vci.go +++ b/auth/api/iam/openid4vci.go @@ -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" ) @@ -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 diff --git a/auth/api/iam/openid4vci_test.go b/auth/api/iam/openid4vci_test.go index da39e4122..5475632db 100644 --- a/auth/api/iam/openid4vci_test.go +++ b/auth/api/iam/openid4vci_test.go @@ -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" @@ -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) @@ -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) @@ -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()), }, } } diff --git a/docs/_static/auth/v2.yaml b/docs/_static/auth/v2.yaml index 254a6cd44..5a4023fb5 100644 --- a/docs/_static/auth/v2.yaml +++ b/docs/_static/auth/v2.yaml @@ -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 diff --git a/docs/pages/release_notes.rst b/docs/pages/release_notes.rst index ac9c1b36c..2f707803c 100644 --- a/docs/pages/release_notes.rst +++ b/docs/pages/release_notes.rst @@ -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) diff --git a/e2e-tests/browser/client/iam/generated.go b/e2e-tests/browser/client/iam/generated.go index 8eb4c6a33..f3e67c12a 100644 --- a/e2e-tests/browser/client/iam/generated.go +++ b/e2e-tests/browser/client/iam/generated.go @@ -277,7 +277,9 @@ type RequestOpenid4VCICredentialIssuanceJSONBody struct { RedirectUri string `json:"redirect_uri"` // WalletDid The DID to which the Verifiable Credential must be issued. Must be owned by the given subject. - WalletDid string `json:"wallet_did"` + // 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. + WalletDid *string `json:"wallet_did,omitempty"` } // RequestServiceAccessTokenParams defines parameters for RequestServiceAccessToken.