Skip to content
Merged
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
14 changes: 7 additions & 7 deletions selfservice/strategy/lookup/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions selfservice/strategy/oidc/strategy_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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) {
Expand Down
14 changes: 7 additions & 7 deletions selfservice/strategy/passkey/passkey_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Down
14 changes: 7 additions & 7 deletions selfservice/strategy/password/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions selfservice/strategy/totp/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions x/err.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
83 changes: 83 additions & 0 deletions x/err_test.go
Original file line number Diff line number Diff line change
@@ -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))
})
}
Loading
Loading