diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go index ce6c9fa58e..73c95f2dbb 100644 --- a/auth/api/iam/openid4vp.go +++ b/auth/api/iam/openid4vp.go @@ -20,6 +20,7 @@ package iam import ( "context" + "encoding/json" "errors" "fmt" "net/http" @@ -293,15 +294,15 @@ func (r Wrapper) handleAuthorizeRequestFromVerifier(ctx context.Context, tenantD } // get verifier metadata - metadata, err := r.auth.IAMClient().ClientMetadata(ctx, params.get(clientMetadataURIParam)) - if err != nil { - return r.sendAndHandleDirectPostError(ctx, oauth.OAuth2Error{Code: oauth.ServerError, Description: "failed to get client metadata (verifier)"}, responseURI, state) + metadata, oauth2Err := r.getClientMetadataFromRequest(ctx, params) + if oauth2Err != nil { + return r.sendAndHandleDirectPostError(ctx, *oauth2Err, responseURI, state) } - // get presentation_definition from presentation_definition_uri - presentationDefinitionURI := params.get(presentationDefUriParam) - presentationDefinition, err := r.auth.IAMClient().PresentationDefinition(ctx, presentationDefinitionURI) - if err != nil { - return r.sendAndHandleDirectPostError(ctx, oauth.OAuth2Error{Code: oauth.InvalidPresentationDefinitionURI, Description: fmt.Sprintf("failed to retrieve presentation definition on %s", presentationDefinitionURI)}, responseURI, state) + + // get presentation_definition + presentationDefinition, oauth2Err := r.getPresentationDefinitionFromRequest(ctx, params) + if oauth2Err != nil { + return r.sendAndHandleDirectPostError(ctx, *oauth2Err, responseURI, state) } // at this point in the flow it would be possible to ask the user to confirm the credentials to use @@ -340,6 +341,47 @@ func (r Wrapper) handleAuthorizeRequestFromVerifier(ctx context.Context, tenantD return r.sendAndHandleDirectPost(ctx, tenantDID, *vp, *submission, responseURI, state) } +func (r Wrapper) getClientMetadataFromRequest(ctx context.Context, params oauthParameters) (*oauth.OAuthClientMetadata, *oauth.OAuth2Error) { + var metadata *oauth.OAuthClientMetadata + var err error + if metadataString := params.get(clientMetadataParam); metadataString != "" { + if params.get(clientMetadataURIParam) != "" { + return nil, &oauth.OAuth2Error{Code: oauth.InvalidRequest, Description: "client_metadata and client_metadata_uri are mutually exclusive", InternalError: err} + } + err = json.Unmarshal([]byte(metadataString), &metadata) + if err != nil { + return nil, &oauth.OAuth2Error{Code: oauth.InvalidRequest, Description: "invalid client_metadata", InternalError: err} + } + } else { + metadata, err = r.auth.IAMClient().ClientMetadata(ctx, params.get(clientMetadataURIParam)) + if err != nil { + return nil, &oauth.OAuth2Error{Code: oauth.ServerError, Description: "failed to get client metadata (verifier)", InternalError: err} + } + } + return metadata, nil +} + +func (r Wrapper) getPresentationDefinitionFromRequest(ctx context.Context, params oauthParameters) (*pe.PresentationDefinition, *oauth.OAuth2Error) { + var presentationDefinition *pe.PresentationDefinition + var err error + if pdString := params.get(presentationDefParam); pdString != "" { + if params.get(presentationDefUriParam) != "" { + return nil, &oauth.OAuth2Error{Code: oauth.InvalidRequest, Description: "presentation_definition and presentation_definition_uri are mutually exclusive"} + } + err = json.Unmarshal([]byte(pdString), &presentationDefinition) + if err != nil { + return nil, &oauth.OAuth2Error{Code: oauth.InvalidRequest, Description: "invalid presentation_definition", InternalError: err} + } + } else { + presentationDefinitionURI := params.get(presentationDefUriParam) + presentationDefinition, err = r.auth.IAMClient().PresentationDefinition(ctx, presentationDefinitionURI) + if err != nil { + return nil, &oauth.OAuth2Error{Code: oauth.InvalidPresentationDefinitionURI, Description: fmt.Sprintf("failed to retrieve presentation definition on %s", presentationDefinitionURI), InternalError: err} + } + } + return presentationDefinition, nil +} + // sendAndHandleDirectPost sends OpenID4VP direct_post to the verifier. The verifier responds with a redirect to the client (including error fields if needed). // If the direct post fails, the user-agent will be redirected back to the client with an error. (Original redirect_uri). func (r Wrapper) sendAndHandleDirectPost(ctx context.Context, walletDID did.DID, vp vc.VerifiablePresentation, presentationSubmission pe.PresentationSubmission, verifierResponseURI string, state string) (HandleAuthorizeRequestResponseObject, error) { diff --git a/auth/api/iam/openid4vp_test.go b/auth/api/iam/openid4vp_test.go index 1105ce988d..34c4bb3825 100644 --- a/auth/api/iam/openid4vp_test.go +++ b/auth/api/iam/openid4vp_test.go @@ -21,12 +21,13 @@ package iam import ( "context" "encoding/json" - "github.com/lestrrat-go/jwx/v2/jwt" "net/http" "net/url" "strings" "testing" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/auth/oauth" @@ -228,6 +229,29 @@ func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) { require.NoError(t, err) }) + t.Run("client_metadata and client_metadata_uri are mutually exclusive", func(t *testing.T) { + ctx := newTestClient(t) + params := defaultParams() + params[clientMetadataParam] = "not empty" + _ = ctx.client.userSessionStore().Put(userSessionID, userSession) + expectPostError(t, ctx, oauth.InvalidRequest, "client_metadata and client_metadata_uri are mutually exclusive", responseURI, "state") + + _, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization) + + require.NoError(t, err) + }) + t.Run("invalid client_metadata", func(t *testing.T) { + ctx := newTestClient(t) + params := defaultParams() + delete(params, clientMetadataURIParam) + params[clientMetadataParam] = "{invalid" + _ = ctx.client.userSessionStore().Put(userSessionID, userSession) + expectPostError(t, ctx, oauth.InvalidRequest, "invalid client_metadata", responseURI, "state") + + _, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization) + + require.NoError(t, err) + }) t.Run("fetching client metadata failed", func(t *testing.T) { ctx := newTestClient(t) params := defaultParams() @@ -257,14 +281,39 @@ func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) { assert.EqualError(t, err, "invalid_request - missing response_uri parameter") }) - t.Run("missing state and missing response_uri", func(t *testing.T) { + t.Run("missing state", func(t *testing.T) { ctx := newTestClient(t) params := defaultParams() - delete(params, responseURIParam) + delete(params, oauth.StateParam) _, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization) - require.Error(t, err) + assert.EqualError(t, err, "invalid_request - missing state parameter") + }) + t.Run("presentation_definition and presentation_definition_uri are mutually exclusive", func(t *testing.T) { + ctx := newTestClient(t) + ctx.iamClient.EXPECT().ClientMetadata(gomock.Any(), "https://example.com/.well-known/authorization-server/iam/verifier").Return(&clientMetadata, nil) + params := defaultParams() + params[presentationDefParam] = "not empty" + _ = ctx.client.userSessionStore().Put(userSessionID, userSession) + expectPostError(t, ctx, oauth.InvalidRequest, "presentation_definition and presentation_definition_uri are mutually exclusive", responseURI, "state") + + _, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization) + + require.NoError(t, err) + }) + t.Run("invalid presentation_definition", func(t *testing.T) { + ctx := newTestClient(t) + params := defaultParams() + delete(params, presentationDefUriParam) + params[presentationDefParam] = "{invalid" + ctx.iamClient.EXPECT().ClientMetadata(gomock.Any(), "https://example.com/.well-known/authorization-server/iam/verifier").Return(&clientMetadata, nil) + _ = ctx.client.userSessionStore().Put(userSessionID, userSession) + expectPostError(t, ctx, oauth.InvalidRequest, "invalid presentation_definition", responseURI, "state") + + _, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization) + + require.NoError(t, err) }) t.Run("invalid presentation_definition_uri", func(t *testing.T) { ctx := newTestClient(t)