From 7a15a06d6934e64a56068156f147f7ca723a0ba2 Mon Sep 17 00:00:00 2001 From: "Marco A." Date: Tue, 31 Mar 2026 17:38:47 +0200 Subject: [PATCH] fix: reducer for PasskeyVerified sets `VerifiedAt` (#11929) # Which Problems Are Solved In https://github.com/zitadel/zitadel/pull/11858#discussion_r2993742914 we discussed on how to translate the ES passkey state to a relational one. That is used to filter out passkeys during convertion to webauthn credentials. It was decided to consider passkeys in ready state those that have a verified at `time.Time` set. The change required fixing the reducer for the eventstore event. # How the Problems Are Solved - Set `VerifiedAt` once `HumanWebAuthNVerifiedEvent` is received - Also set the verification to `nil` # Additional Context - Discussion #11858 - Follow-up for PR #11858 --- .../postgres/migration/013_user_table/up.sql | 10 ++-- internal/query/projection/assert.go | 4 +- internal/query/projection/user_relational.go | 1 + .../query/projection/user_relational_test.go | 60 +++++++++++++++++++ 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/backend/v3/storage/database/dialect/postgres/migration/013_user_table/up.sql b/backend/v3/storage/database/dialect/postgres/migration/013_user_table/up.sql index 184fdeb78..440af9bb5 100644 --- a/backend/v3/storage/database/dialect/postgres/migration/013_user_table/up.sql +++ b/backend/v3/storage/database/dialect/postgres/migration/013_user_table/up.sql @@ -185,7 +185,7 @@ CREATE TYPE zitadel.passkey_type AS ENUM ( CREATE TABLE zitadel.user_passkeys( instance_id TEXT NOT NULL , token_id TEXT NOT NULL - , key_id BYTEA NOT NULL + , key_id BYTEA , user_id TEXT NOT NULL @@ -194,12 +194,12 @@ CREATE TABLE zitadel.user_passkeys( , verified_at TIMESTAMPTZ , type zitadel.passkey_type NOT NULL - , name TEXT NOT NULL CHECK (name <> '') + , name TEXT NOT NULL DEFAULT '' , sign_count INT NOT NULL DEFAULT 0 CHECK (sign_count >= 0) , challenge BYTEA NOT NULL - , public_key BYTEA NOT NULL - , attestation_type TEXT NOT NULL CHECK (attestation_type <> '') - , authenticator_attestation_guid BYTEA NOT NULL + , public_key BYTEA + , attestation_type TEXT NOT NULL + , authenticator_attestation_guid BYTEA , relying_party_id TEXT NOT NULL CHECK (relying_party_id <> '') , PRIMARY KEY (instance_id, token_id) diff --git a/internal/query/projection/assert.go b/internal/query/projection/assert.go index 96ed19543..7705b867b 100644 --- a/internal/query/projection/assert.go +++ b/internal/query/projection/assert.go @@ -1,6 +1,8 @@ package projection import ( + "fmt" + "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -8,7 +10,7 @@ import ( func assertEvent[T eventstore.Event](event eventstore.Event) (T, error) { e, ok := event.(T) if !ok { - return e, zerrors.ThrowInvalidArgumentf(nil, "HANDL-1m9fS", "reduce.wrong.event.type %T", event) + return e, zerrors.CreateZitadelError(zerrors.KindInvalidArgument, nil, "HANDL-1m9fS", fmt.Sprintf("reduce.wrong.event.type %T", event), 1) } return e, nil } diff --git a/internal/query/projection/user_relational.go b/internal/query/projection/user_relational.go index 5448a496a..12d8c02a6 100644 --- a/internal/query/projection/user_relational.go +++ b/internal/query/projection/user_relational.go @@ -1124,6 +1124,7 @@ func (p *relationalTablesProjection) reducePasskeyVerified(event eventstore.Even repo.SetPasskeySignCount(e.SignCount), repo.SetPasskeyName(e.WebAuthNTokenName), repo.SetPasskeyUpdatedAt(e.CreatedAt()), + repo.SetPasskeyVerifiedAt(e.CreatedAt()), ), repo.SetUpdatedAt(e.CreatedAt()), ) diff --git a/internal/query/projection/user_relational_test.go b/internal/query/projection/user_relational_test.go index 1328e852d..ebd33edd9 100644 --- a/internal/query/projection/user_relational_test.go +++ b/internal/query/projection/user_relational_test.go @@ -181,6 +181,66 @@ func TestUserRelationalProjection_Reducers(t *testing.T) { assert.Zero(t, gotUser.Human.RecoveryCodes.LastSuccessfullyCheckedAt) assert.Equal(t, uint8(1), gotUser.Human.RecoveryCodes.FailedAttempts) }) + + t.Run("reduce user.HumanWebAuthNVerifiedEvent event", func(t *testing.T) { + // Given + + // create user + existingUserID := gofakeit.UUID() + existingUserAgg := createUser(t, tx, userRepo, instanceID, orgID, existingUserID) + now := time.Now() + + // add passkey to the user + pkey := &domain.Passkey{ + ID: "pkey-id", + Challenge: []byte("some challenge"), + RelyingPartyID: "rpID", + CreatedAt: now, + UpdatedAt: now, + Type: domain.PasskeyTypePasswordless, + } + + _, err = userRepo.Update(t.Context(), tx, + userRepo.PrimaryKeyCondition(instanceID, existingUserID), + userRepo.Human().AddPasskey(pkey), + ) + require.NoError(t, err) + + pkeyVerifiedEvt := user.NewHumanPasswordlessVerifiedEvent( + t.Context(), + &existingUserAgg.Aggregate, + pkey.ID, + "some-token-name", + "attestation-type", + []byte("key-id"), + []byte("public-key-id"), + []byte("authenticator-attestation-guid"), + 33, + "", + ) + + // Test + eventReduced := callReduce(t, rawTx, handler, pkeyVerifiedEvt) + require.True(t, eventReduced) + + // Verify + gotUser, err := userRepo.Get(t.Context(), tx, database.WithCondition( + database.And( + userRepo.IDCondition(existingUserID), + userRepo.InstanceIDCondition(instanceID), + ), + )) + require.NoError(t, err) + require.Len(t, gotUser.Human.Passkeys, 1) + gotPKey := gotUser.Human.Passkeys[0] + assert.Equal(t, pkeyVerifiedEvt.KeyID, gotPKey.KeyID) + assert.Equal(t, pkeyVerifiedEvt.PublicKey, gotPKey.PublicKey) + assert.Equal(t, pkeyVerifiedEvt.AttestationType, gotPKey.AttestationType) + assert.Equal(t, pkeyVerifiedEvt.AAGUID, gotPKey.AuthenticatorAttestationGUID) + assert.Equal(t, pkeyVerifiedEvt.SignCount, gotPKey.SignCount) + assert.Equal(t, pkeyVerifiedEvt.WebAuthNTokenName, gotPKey.Name) + assert.NotZero(t, gotPKey.VerifiedAt) + }) } func createUser(t *testing.T,