diff --git a/auth/api/auth/v1/api_test.go b/auth/api/auth/v1/api_test.go index 3310ad2ba9..17354201fe 100644 --- a/auth/api/auth/v1/api_test.go +++ b/auth/api/auth/v1/api_test.go @@ -74,6 +74,10 @@ func (m *mockAuthClient) AuthorizationEndpointEnabled() bool { return true } +func (m *mockAuthClient) OpenID4VCIEnabled() bool { + return true +} + func (m *mockAuthClient) AuthzServer() oauth.AuthorizationServer { return m.authzServer } diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go index 405a52e7b3..1493fd2eac 100644 --- a/auth/api/iam/openid4vp.go +++ b/auth/api/iam/openid4vp.go @@ -23,7 +23,10 @@ import ( "encoding/json" "errors" "fmt" + "github.com/nuts-foundation/nuts-node/http/user" + "github.com/nuts-foundation/nuts-node/vcr/types" + "net/http" "net/url" "slices" @@ -536,7 +539,7 @@ func (r Wrapper) handleAuthorizeResponseSubmission(ctx context.Context, request if err != nil { return nil, oauth.OAuth2Error{ Code: oauth.InvalidRequest, - Description: "presentation(s) or contained credential(s) are invalid", + Description: verificationErrorDescription(err), InternalError: err, RedirectURI: callbackURI, } @@ -764,3 +767,17 @@ func oauthError(code oauth.ErrorCode, description string, internalError ...error InternalError: errors.Join(internalError...), } } + +// verificationErrorDescription returns a more specific error description when DID resolution fails, +// otherwise returns the generic error message. This improves user experience by providing actionable +// error information for common DID resolution issues while maintaining security for other errors. +func verificationErrorDescription(err error) string { + // Check for DID resolution errors + if errors.Is(err, resolver.ErrNotFound) || errors.Is(err, resolver.ErrKeyNotFound) || strings.Contains(err.Error(), "unable to resolve") || + errors.Is(err, types.ErrStatusNotFound) || errors.Is(err, types.ErrRevoked) || errors.Is(err, types.ErrCredentialNotValidAtTime) || errors.Is(err, types.ErrPresentationNotValidAtTime) { + return "presentation(s) or credential(s) verification failed: " + err.Error() + } + + // Default generic message for other errors + return "presentation(s) or credential(s) verification failed" +} diff --git a/auth/api/iam/openid4vp_test.go b/auth/api/iam/openid4vp_test.go index f57ff74a73..c604ac1afc 100644 --- a/auth/api/iam/openid4vp_test.go +++ b/auth/api/iam/openid4vp_test.go @@ -21,6 +21,8 @@ package iam import ( "context" "encoding/json" + "errors" + "fmt" "github.com/nuts-foundation/nuts-node/http/user" "net/http" "net/url" @@ -35,6 +37,7 @@ import ( "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/test" "github.com/nuts-foundation/nuts-node/vcr/pe" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -964,5 +967,31 @@ func putNonce(ctx *testCtx, nonce string) { func putCodeSession(ctx *testCtx, code string, oauthSession OAuthSession) { _ = ctx.client.oauthCodeStore().Put(code, oauthSession) +} + +func Test_verificationErrorDescription(t *testing.T) { + t.Run("DID not found error", func(t *testing.T) { + err := resolver.ErrNotFound + result := verificationErrorDescription(err) + assert.Equal(t, "presentation(s) or contained credential(s) are invalid: unable to find the DID document", result) + }) + + t.Run("Key not found error", func(t *testing.T) { + err := resolver.ErrKeyNotFound + result := verificationErrorDescription(err) + assert.Equal(t, "presentation(s) or contained credential(s) are invalid: key not found in DID document", result) + }) + t.Run("Wrapped DID resolution error", func(t *testing.T) { + err := fmt.Errorf("unable to resolve valid signing key: %w", resolver.ErrNotFound) + result := verificationErrorDescription(err) + assert.Equal(t, "presentation(s) or contained credential(s) are invalid: unable to resolve valid signing key: unable to find the DID document", result) + }) + + t.Run("Generic error returns default message", func(t *testing.T) { + err := errors.New("some other error") + result := verificationErrorDescription(err) + assert.Equal(t, "presentation(s) or contained credential(s) are invalid", result) + }) } + diff --git a/auth/api/iam/s2s_vptoken.go b/auth/api/iam/s2s_vptoken.go index 16018dbc31..c215ea4269 100644 --- a/auth/api/iam/s2s_vptoken.go +++ b/auth/api/iam/s2s_vptoken.go @@ -101,7 +101,7 @@ func (r Wrapper) handleS2SAccessTokenRequest(ctx context.Context, clientID strin if err != nil { return nil, oauth.OAuth2Error{ Code: oauth.InvalidRequest, - Description: "presentation(s) or contained credential(s) are invalid", + Description: verificationErrorDescription(err), InternalError: err, } } diff --git a/vcr/pe/schema/gen/go.mod b/vcr/pe/schema/gen/go.mod index b872a8ae8e..05c918dc65 100644 --- a/vcr/pe/schema/gen/go.mod +++ b/vcr/pe/schema/gen/go.mod @@ -2,6 +2,4 @@ module github.com/nuts-foundation/nuts-node/vcr/pe/gen/schema go 1.21 -require ( - github.com/a-h/generate v0.0.0-20220105161013-96c14dfdfb60 // indirect -) +require github.com/a-h/generate v0.0.0-20220105161013-96c14dfdfb60 diff --git a/vcr/pe/schema/gen/go.sum b/vcr/pe/schema/gen/go.sum index 1f994a18a1..d5af305fbb 100644 --- a/vcr/pe/schema/gen/go.sum +++ b/vcr/pe/schema/gen/go.sum @@ -1,16 +1,2 @@ -github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8= -github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I= -github.com/PaesslerAG/gval v1.2.2 h1:Y7iBzhgE09IGTt5QgGQ2IdaYYYOU134YGHBThD+wm9E= -github.com/PaesslerAG/gval v1.2.2/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac= -github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= -github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= -github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= -github.com/PaesslerAG/jsonpath v0.1.2-0.20230323094847-3484786d6f97 h1:XIsQOSBJi/9Bexr+rjUpuYi0IkQ+YqNKKlE7Yt/sw9Q= -github.com/PaesslerAG/jsonpath v0.1.2-0.20230323094847-3484786d6f97/go.mod h1:zTyVtYhYjcHpfCtqnCMxejgp0pEEwb/xJzhn05NrkJk= github.com/a-h/generate v0.0.0-20220105161013-96c14dfdfb60 h1:/rNdG6EuzjwcR1KRFpF+9qWmWh2xIcz84QOeMGr/2L8= github.com/a-h/generate v0.0.0-20220105161013-96c14dfdfb60/go.mod h1:traiLYQ0YD7qUMCdjo6/jSaJRPHXniX4HVs+PhEhYpc= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=