From 772572c0d633d5848f12d41947d52df1d391329b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz?= <12242002+mszekiel@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:30:21 +0100 Subject: [PATCH] feat: trace identity id in errors GitOrigin-RevId: dec38a20911ebf8038ed8eb5a5f286e79430266d --- selfservice/strategy/lookup/login.go | 14 +- selfservice/strategy/oidc/strategy_login.go | 6 +- selfservice/strategy/passkey/passkey_login.go | 14 +- selfservice/strategy/password/login.go | 14 +- selfservice/strategy/totp/login.go | 8 +- x/err.go | 29 ++++ x/err_test.go | 83 ++++++++++ x/events/events.go | 98 +++++++---- x/events/events_test.go | 154 ++++++++++++++++++ 9 files changed, 356 insertions(+), 64 deletions(-) create mode 100644 x/err_test.go diff --git a/selfservice/strategy/lookup/login.go b/selfservice/strategy/lookup/login.go index 2668774a62d2..0d487e18a3a6 100644 --- a/selfservice/strategy/lookup/login.go +++ b/selfservice/strategy/lookup/login.go @@ -126,7 +126,7 @@ func (s *Strategy) Login(_ http.ResponseWriter, r *http.Request, f *login.Flow, var o identity.CredentialsLookupConfig if err := json.Unmarshal(c.Config, &o); err != nil { - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("The lookup secrets could not be decoded properly").WithDebug(err.Error()).WithWrap(err)) + return nil, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError.WithReason("The lookup secrets could not be decoded properly").WithDebug(err.Error()).WithWrap(err)), i.ID) } var found bool @@ -136,24 +136,24 @@ func (s *Strategy) Login(_ http.ResponseWriter, r *http.Request, f *login.Flow, o.RecoveryCodes[k].UsedAt = sqlxx.NullTime(time.Now().UTC().Round(time.Second)) found = true } else { - return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewLookupAlreadyUsed())) + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(schema.NewLookupAlreadyUsed()), i.ID)) } } } if !found { - return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewErrorValidationLookupInvalid())) + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(schema.NewErrorValidationLookupInvalid()), i.ID)) } // We can't use a transaction here because HydrateIdentityAssociations (used by update) does not support transactions. toUpdate, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, sess.IdentityID) if err != nil { - return nil, s.handleLoginError(r, f, err) + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(err, i.ID)) } encoded, err := json.Marshal(&o) if err != nil { - return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to encode updated lookup secrets.").WithDebug(err.Error()))) + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to encode updated lookup secrets.").WithDebug(err.Error())), i.ID)) } c.Config = encoded @@ -164,12 +164,12 @@ func (s *Strategy) Login(_ http.ResponseWriter, r *http.Request, f *login.Flow, // We need to allow write protected traits because we are updating the lookup secrets. identity.ManagerAllowWriteProtectedTraits, ); err != nil { - return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to update identity.").WithDebug(err.Error()))) + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to update identity.").WithDebug(err.Error())), i.ID)) } f.Active = s.ID() if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { - return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow.").WithDebug(err.Error()))) + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow.").WithDebug(err.Error())), i.ID)) } return i, nil diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index 74a3d05e1801..bb704d6df62b 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -240,7 +240,7 @@ func (s *Strategy) ProcessLogin(ctx context.Context, w http.ResponseWriter, r *h var oidcCredentials identity.CredentialsOIDC if err := json.NewDecoder(bytes.NewBuffer(c.Config)).Decode(&oidcCredentials); err != nil { - return nil, s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("The OpenID Connect credentials could not be decoded properly").WithDebug(err.Error()))) + return nil, s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError.WithReason("The OpenID Connect credentials could not be decoded properly").WithDebug(err.Error())), i.ID)) } sess := session.NewInactiveSession() @@ -249,13 +249,13 @@ func (s *Strategy) ProcessLogin(ctx context.Context, w http.ResponseWriter, r *h for _, c := range oidcCredentials.Providers { if c.Subject == claims.Subject && c.Provider == provider.Config().ID { if err = s.d.LoginHookExecutor().PostLoginHook(w, r, node.OpenIDConnectGroup, loginFlow, i, sess, provider.Config().ID); err != nil { - return nil, s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err) + return nil, x.WrapWithIdentityIDError(s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err), i.ID) } return nil, nil } } - return nil, s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to find matching OpenID Connect credentials.").WithDebugf(`Unable to find credentials that match the given provider "%s" and subject "%s".`, provider.Config().ID, claims.Subject))) + return nil, s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to find matching OpenID Connect credentials.").WithDebugf(`Unable to find credentials that match the given provider "%s" and subject "%s".`, provider.Config().ID, claims.Subject)), i.ID)) } func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ *session.Session) (i *identity.Identity, err error) { diff --git a/selfservice/strategy/passkey/passkey_login.go b/selfservice/strategy/passkey/passkey_login.go index 8ea065e48d87..bc2827580545 100644 --- a/selfservice/strategy/passkey/passkey_login.go +++ b/selfservice/strategy/passkey/passkey_login.go @@ -250,9 +250,9 @@ func (s *Strategy) loginAuthenticate(ctx context.Context, r *http.Request, f *lo } err = s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, i, identity.ExpandCredentials) if err != nil { - return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError. + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError. WithReason("Could not load identity credentials"). - WithWrap(err))) + WithWrap(err)), i.ID)) } c, ok := i.GetCredentials(credentialType) @@ -262,10 +262,10 @@ func (s *Strategy) loginAuthenticate(ctx context.Context, r *http.Request, f *lo var o identity.CredentialsWebAuthnConfig if err := json.Unmarshal(c.Config, &o); err != nil { - return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError. + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError. WithReason("The WebAuthn credentials could not be decoded properly"). WithDebug(err.Error()). - WithWrap(err))) + WithWrap(err)), i.ID)) } webAuthCreds := o.Credentials.PasswordlessOnly(&webAuthnResponse.Response.AuthenticatorData.Flags) @@ -274,18 +274,18 @@ func (s *Strategy) loginAuthenticate(ctx context.Context, r *http.Request, f *lo return webauthnx.NewUser(userHandle, webAuthCreds, web.Config), nil }, webAuthnSess, webAuthnResponse) if err != nil { - return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewWebAuthnVerifierWrongError("#/"))) + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(schema.NewWebAuthnVerifierWrongError("#/")), i.ID)) } // Remove the WebAuthn URL from the internal context now that it is set! f.InternalContext, err = sjson.DeleteBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) if err != nil { - return nil, s.handleLoginError(r, f, errors.WithStack(err)) + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(err), i.ID)) } f.Active = s.ID() if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { - return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error())), i.ID)) } return i, nil diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index 4f1b9222775f..8c83a8149c33 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -87,13 +87,13 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, var o identity.CredentialsPassword d := json.NewDecoder(bytes.NewBuffer(c.Config)) if err := d.Decode(&o); err != nil { - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err)) + return nil, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err)), i.ID) } if o.ShouldUsePasswordMigrationHook() { pwHook := s.d.Config().PasswordMigrationHook(ctx) if !pwHook.Enabled { - return nil, errors.WithStack(herodot.ErrMisconfiguration.WithReasonf("Password migration hook is not enabled but password migration is requested.")) + return nil, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrMisconfiguration.WithReasonf("Password migration hook is not enabled but password migration is requested.")), i.ID) } migrationHook := hook.NewPasswordMigrationHook(s.d, &pwHook.Config) @@ -103,27 +103,27 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, Identity: i, }) if err != nil { - return nil, s.handleLoginError(r, f, p, err) + return nil, s.handleLoginError(r, f, p, x.WrapWithIdentityIDError(err, i.ID)) } if err := s.migratePasswordHash(ctx, i.ID, []byte(p.Password)); err != nil { - return nil, s.handleLoginError(r, f, p, err) + return nil, s.handleLoginError(r, f, p, x.WrapWithIdentityIDError(err, i.ID)) } } else { if err := hash.Compare(ctx, []byte(p.Password), []byte(o.HashedPassword)); err != nil { - return nil, s.handleLoginError(r, f, p, errors.WithStack(schema.NewInvalidCredentialsError())) + return nil, s.handleLoginError(r, f, p, errors.WithStack(x.WrapWithIdentityIDError(schema.NewInvalidCredentialsError(), i.ID))) } if !s.d.Hasher(ctx).Understands([]byte(o.HashedPassword)) { if err := s.migratePasswordHash(ctx, i.ID, []byte(p.Password)); err != nil { - s.d.Logger().Warnf("Unable to migrate password hash for identity %s: %s Keeping existing password hash and continuing.", i.ID, err) + s.d.Logger().Warnf("Unable to migrate password hash for identity %s: %s Keeping existing password hash and continuing.", i.ID, x.WrapWithIdentityIDError(err, i.ID)) } } } f.Active = s.ID() if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { - return nil, s.handleLoginError(r, f, p, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) + return nil, s.handleLoginError(r, f, p, errors.WithStack(x.WrapWithIdentityIDError(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()), i.ID))) } return i, nil diff --git a/selfservice/strategy/totp/login.go b/selfservice/strategy/totp/login.go index a05443206cf6..90e32d681387 100644 --- a/selfservice/strategy/totp/login.go +++ b/selfservice/strategy/totp/login.go @@ -127,21 +127,21 @@ func (s *Strategy) Login(_ http.ResponseWriter, r *http.Request, f *login.Flow, var o identity.CredentialsTOTPConfig if err := json.Unmarshal(c.Config, &o); err != nil { - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("The TOTP credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err)) + return nil, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError.WithReason("The TOTP credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err)), i.ID) } key, err := otp.NewKeyFromURL(o.TOTPURL) if err != nil { - return nil, s.handleLoginError(r, f, errors.WithStack(err)) + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(err), i.ID)) } if !totp.Validate(p.TOTPCode, key.Secret()) { - return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewTOTPVerifierWrongError("#/"))) + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(schema.NewTOTPVerifierWrongError("#/")), i.ID)) } f.Active = s.ID() if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { - return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) + return nil, s.handleLoginError(r, f, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error())), i.ID)) } return i, nil diff --git a/x/err.go b/x/err.go index 5b3868734cca..5f783d07f096 100644 --- a/x/err.go +++ b/x/err.go @@ -7,9 +7,38 @@ import ( "errors" "net/http" + "github.com/gofrs/uuid" "github.com/ory/herodot" ) +type WithIdentityIDError struct { + err error + identityID uuid.UUID +} + +func (e *WithIdentityIDError) Error() string { + return e.err.Error() +} + +func (e *WithIdentityIDError) Unwrap() error { + return e.err +} + +func (e *WithIdentityIDError) IdentityID() uuid.UUID { + return e.identityID +} + +func WrapWithIdentityIDError(err error, identityID uuid.UUID) error { + if err == nil { + return nil + } + + return &WithIdentityIDError{ + err: err, + identityID: identityID, + } +} + var ( PseudoPanic = herodot.DefaultError{ StatusField: http.StatusText(http.StatusInternalServerError), diff --git a/x/err_test.go b/x/err_test.go new file mode 100644 index 000000000000..94374063c133 --- /dev/null +++ b/x/err_test.go @@ -0,0 +1,83 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x_test + +import ( + "errors" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/x" +) + +func TestWrapWithIdentityIDError(t *testing.T) { + t.Run("case=wraps error with identity ID", func(t *testing.T) { + baseErr := errors.New("test error") + identityID := uuid.Must(uuid.NewV4()) + + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + + require.NotNil(t, wrappedErr) + assert.Equal(t, "test error", wrappedErr.Error()) + + var withIDErr *x.WithIdentityIDError + require.True(t, errors.As(wrappedErr, &withIDErr)) + assert.Equal(t, identityID, withIDErr.IdentityID()) + }) + + t.Run("case=unwraps to original error", func(t *testing.T) { + baseErr := errors.New("original error") + identityID := uuid.Must(uuid.NewV4()) + + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + + unwrappedErr := errors.Unwrap(wrappedErr) + assert.Equal(t, baseErr, unwrappedErr) + }) + + t.Run("case=returns nil when wrapping nil error", func(t *testing.T) { + identityID := uuid.Must(uuid.NewV4()) + + wrappedErr := x.WrapWithIdentityIDError(nil, identityID) + + assert.Nil(t, wrappedErr) + }) + + t.Run("case=preserves identity ID with nil UUID", func(t *testing.T) { + baseErr := errors.New("test error") + var identityID uuid.UUID // nil UUID + + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + + var withIDErr *x.WithIdentityIDError + require.True(t, errors.As(wrappedErr, &withIDErr)) + assert.Equal(t, uuid.Nil, withIDErr.IdentityID()) + }) + + t.Run("case=can wrap already wrapped error", func(t *testing.T) { + baseErr := errors.New("base error") + firstID := uuid.Must(uuid.NewV4()) + secondID := uuid.Must(uuid.NewV4()) + + firstWrap := x.WrapWithIdentityIDError(baseErr, firstID) + secondWrap := x.WrapWithIdentityIDError(firstWrap, secondID) + + var withIDErr *x.WithIdentityIDError + require.True(t, errors.As(secondWrap, &withIDErr)) + // Should get the outermost identity ID + assert.Equal(t, secondID, withIDErr.IdentityID()) + }) + + t.Run("case=works with errors.Is", func(t *testing.T) { + baseErr := errors.New("base error") + identityID := uuid.Must(uuid.NewV4()) + + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + + assert.True(t, errors.Is(wrappedErr, baseErr)) + }) +} diff --git a/x/events/events.go b/x/events/events.go index cbd28f351e31..f92602bd5900 100644 --- a/x/events/events.go +++ b/x/events/events.go @@ -15,6 +15,7 @@ import ( "github.com/ory/herodot" "github.com/ory/kratos/schema" + "github.com/ory/kratos/x" "github.com/ory/x/jsonx" "github.com/ory/x/otelx/semconv" ) @@ -326,39 +327,58 @@ func NewRegistrationFailed(ctx context.Context, flowID uuid.UUID, flowType, meth } func NewRecoveryFailed(ctx context.Context, flowID uuid.UUID, flowType, method string, err error) (string, trace.EventOption) { - return RecoveryFailed.String(), - trace.WithAttributes(append( - semconv.AttributesFromContext(ctx), - attrSelfServiceFlowType(flowType), - attrSelfServiceMethodUsed(method), - attrReason(err), - attrErrorReason(err), - attrFlowID(flowID), - )...) + attrs := append( + semconv.AttributesFromContext(ctx), + attrSelfServiceFlowType(flowType), + attrSelfServiceMethodUsed(method), + attrReason(err), + attrErrorReason(err), + attrFlowID(flowID), + ) + + var identityIDError *x.WithIdentityIDError + if errors.As(err, &identityIDError) { + attrs = append(attrs, semconv.AttrIdentityID(identityIDError.IdentityID())) + } + + return RecoveryFailed.String(), trace.WithAttributes(attrs...) } func NewSettingsFailed(ctx context.Context, flowID uuid.UUID, flowType, method string, err error) (string, trace.EventOption) { - return SettingsFailed.String(), - trace.WithAttributes(append( - semconv.AttributesFromContext(ctx), - attrSelfServiceFlowType(flowType), - attrSelfServiceMethodUsed(method), - attrReason(err), - attrErrorReason(err), - attrFlowID(flowID), - )...) + attrs := append( + semconv.AttributesFromContext(ctx), + attrSelfServiceFlowType(flowType), + attrSelfServiceMethodUsed(method), + attrReason(err), + attrErrorReason(err), + attrFlowID(flowID), + ) + + var identityIDError *x.WithIdentityIDError + if errors.As(err, &identityIDError) { + attrs = append(attrs, semconv.AttrIdentityID(identityIDError.IdentityID())) + } + + return SettingsFailed.String(), trace.WithAttributes(attrs...) } func NewVerificationFailed(ctx context.Context, flowID uuid.UUID, flowType, method string, err error) (string, trace.EventOption) { + attrs := append( + semconv.AttributesFromContext(ctx), + attrSelfServiceFlowType(flowType), + attrSelfServiceMethodUsed(method), + attrReason(err), + attrErrorReason(err), + attrFlowID(flowID), + ) + + var identityIDError *x.WithIdentityIDError + if errors.As(err, &identityIDError) { + attrs = append(attrs, semconv.AttrIdentityID(identityIDError.IdentityID())) + } + return VerificationFailed.String(), - trace.WithAttributes(append( - semconv.AttributesFromContext(ctx), - attrSelfServiceFlowType(flowType), - attrSelfServiceMethodUsed(method), - attrReason(err), - attrErrorReason(err), - attrFlowID(flowID), - )...) + trace.WithAttributes(attrs...) } func NewIdentityCreated(ctx context.Context, identityID uuid.UUID) (string, trace.EventOption) { @@ -392,16 +412,22 @@ func NewIdentityUpdated(ctx context.Context, identityID uuid.UUID) (string, trac } func NewLoginFailed(ctx context.Context, flowID uuid.UUID, flowType, requestedAAL string, isRefresh bool, err error) (string, trace.EventOption) { - return LoginFailed.String(), - trace.WithAttributes(append( - semconv.AttributesFromContext(ctx), - attrSelfServiceFlowType(flowType), - attLoginRequestedAAL(requestedAAL), - attLoginRequestedPrivilegedSession(isRefresh), - attrReason(err), - attrErrorReason(err), - attrFlowID(flowID), - )...) + attrs := append( + semconv.AttributesFromContext(ctx), + attrSelfServiceFlowType(flowType), + attLoginRequestedAAL(requestedAAL), + attLoginRequestedPrivilegedSession(isRefresh), + attrReason(err), + attrErrorReason(err), + attrFlowID(flowID), + ) + + var identityIDError *x.WithIdentityIDError + if errors.As(err, &identityIDError) { + attrs = append(attrs, semconv.AttrIdentityID(identityIDError.IdentityID())) + } + + return LoginFailed.String(), trace.WithAttributes(attrs...) } func NewSessionRevoked(ctx context.Context, sessionID, identityID uuid.UUID) (string, trace.EventOption) { diff --git a/x/events/events_test.go b/x/events/events_test.go index 4d4f1fca4da3..614b6cf9894a 100644 --- a/x/events/events_test.go +++ b/x/events/events_test.go @@ -7,11 +7,13 @@ import ( "errors" "testing" + "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "github.com/ory/kratos/identity" + "github.com/ory/kratos/x" "github.com/ory/kratos/x/events" ) @@ -74,3 +76,155 @@ func TestNewJsonnetMappingFailed(t *testing.T) { }) } } + +func TestNewLoginFailed(t *testing.T) { + ctx := t.Context() + flowID := uuid.Must(uuid.NewV4()) + identityID := uuid.Must(uuid.NewV4()) + baseErr := errors.New("login failed") + + t.Run("case=without identity ID", func(t *testing.T) { + eventName, opts := events.NewLoginFailed(ctx, flowID, "browser", "aal1", false, baseErr) + + assert.Equal(t, events.LoginFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + // Should not contain IdentityID attribute + for _, attr := range attrs { + assert.NotEqual(t, "IdentityID", string(attr.Key)) + } + + // Should contain other attributes + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("LoginRequestedAAL", "aal1")) + assert.Contains(t, attrs, attribute.Bool("LoginRequestedPrivilegedSession", false)) + assert.Contains(t, attrs, attribute.String("ErrorReason", "login failed")) + }) + + t.Run("case=with identity ID", func(t *testing.T) { + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + eventName, opts := events.NewLoginFailed(ctx, flowID, "browser", "aal1", false, wrappedErr) + + assert.Equal(t, events.LoginFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + assert.Contains(t, attrs, attribute.String("IdentityID", identityID.String())) + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("ErrorReason", "login failed")) + }) +} + +func TestNewRecoveryFailed(t *testing.T) { + ctx := t.Context() + flowID := uuid.Must(uuid.NewV4()) + identityID := uuid.Must(uuid.NewV4()) + baseErr := errors.New("recovery failed") + + t.Run("case=without identity ID", func(t *testing.T) { + eventName, opts := events.NewRecoveryFailed(ctx, flowID, "browser", "code", baseErr) + + assert.Equal(t, events.RecoveryFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + for _, attr := range attrs { + assert.NotEqual(t, "IdentityID", string(attr.Key)) + } + + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("SelfServiceMethodUsed", "code")) + }) + + t.Run("case=with identity ID", func(t *testing.T) { + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + eventName, opts := events.NewRecoveryFailed(ctx, flowID, "browser", "link", wrappedErr) + + assert.Equal(t, events.RecoveryFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + assert.Contains(t, attrs, attribute.String("IdentityID", identityID.String())) + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("SelfServiceMethodUsed", "link")) + }) +} + +func TestNewSettingsFailed(t *testing.T) { + ctx := t.Context() + flowID := uuid.Must(uuid.NewV4()) + identityID := uuid.Must(uuid.NewV4()) + baseErr := errors.New("settings failed") + + t.Run("case=without identity ID", func(t *testing.T) { + eventName, opts := events.NewSettingsFailed(ctx, flowID, "browser", "profile", baseErr) + + assert.Equal(t, events.SettingsFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + for _, attr := range attrs { + assert.NotEqual(t, "IdentityID", string(attr.Key)) + } + + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("SelfServiceMethodUsed", "profile")) + }) + + t.Run("case=with identity ID", func(t *testing.T) { + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + eventName, opts := events.NewSettingsFailed(ctx, flowID, "browser", "password", wrappedErr) + + assert.Equal(t, events.SettingsFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + assert.Contains(t, attrs, attribute.String("IdentityID", identityID.String())) + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("SelfServiceMethodUsed", "password")) + }) +} + +func TestNewVerificationFailed(t *testing.T) { + ctx := t.Context() + flowID := uuid.Must(uuid.NewV4()) + identityID := uuid.Must(uuid.NewV4()) + baseErr := errors.New("verification failed") + + t.Run("case=without identity ID", func(t *testing.T) { + eventName, opts := events.NewVerificationFailed(ctx, flowID, "browser", "code", baseErr) + + assert.Equal(t, events.VerificationFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + for _, attr := range attrs { + assert.NotEqual(t, "IdentityID", string(attr.Key)) + } + + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("SelfServiceMethodUsed", "code")) + }) + + t.Run("case=with identity ID", func(t *testing.T) { + wrappedErr := x.WrapWithIdentityIDError(baseErr, identityID) + eventName, opts := events.NewVerificationFailed(ctx, flowID, "browser", "link", wrappedErr) + + assert.Equal(t, events.VerificationFailed.String(), eventName) + + eventConfig := trace.NewEventConfig(opts) + attrs := eventConfig.Attributes() + + assert.Contains(t, attrs, attribute.String("IdentityID", identityID.String())) + assert.Contains(t, attrs, attribute.String("SelfServiceFlowType", "browser")) + assert.Contains(t, attrs, attribute.String("SelfServiceMethodUsed", "link")) + }) +}