Skip to content

Add passkey model layer

Related to #569927

What does this MR do and why?

Adds the passkey model layer

  • Adds the :authentication_mode, :passkey_eligible, and :last_used_at columns with appropriate validation in the WebauthnRegistration table
  • Connects to the User model via associations
  • Updates the WebauthnRegistration factory

Database Review

Query Plans (Backwards-compatibility)

Table
has_many Query Plan
Initial (:webauthn_registrations) link
Post-migration (:webauthn_registrations) link
Post-migration (:passkeys) link

Local Migrations

bin/rails db:migrate
main: == [advisory_lock_connection] object_id: 156460, pg_backend_pid: 63025
main: == 20250926104422 AddPasskeyColumnsToWebauthnRegistrations: migrating =========
main: -- add_column(:webauthn_registrations, :authentication_mode, :integer, {:default=>1, :null=>false})
main:    -> 0.0483s
main: -- add_column(:webauthn_registrations, :passkey_eligible, :boolean, {:default=>false, :null=>false})
main:    -> 0.0023s
main: -- add_column(:webauthn_registrations, :last_used_at, :datetime_with_timezone)
main:    -> 0.0071s
main: == 20250926104422 AddPasskeyColumnsToWebauthnRegistrations: migrated (0.0631s) 
main: == [advisory_lock_connection] object_id: 156460, pg_backend_pid: 63025
ci: == [advisory_lock_connection] object_id: 156460, pg_backend_pid: 63026
ci: == 20250926104422 AddPasskeyColumnsToWebauthnRegistrations: migrating =========
ci: -- add_column(:webauthn_registrations, :authentication_mode, :integer, {:default=>1, :null=>false})
ci:    -> 0.0086s
ci: -- add_column(:webauthn_registrations, :passkey_eligible, :boolean, {:default=>false, :null=>false})
ci:    -> 0.0028s
ci: -- add_column(:webauthn_registrations, :last_used_at, :datetime_with_timezone)
ci:    -> 0.0012s
ci: == 20250926104422 AddPasskeyColumnsToWebauthnRegistrations: migrated (0.0233s) 
ci: == [advisory_lock_connection] object_id: 156460, pg_backend_pid: 63026
VERSION=20250926104422 bin/rails db:migrate:down:main && VERSION=20250926104422 bin/rails db:migrate:down:ci
main: == [advisory_lock_connection] object_id: 156160, pg_backend_pid: 64613
main: == 20250926104422 AddPasskeyColumnsToWebauthnRegistrations: reverting =========
main: -- remove_column(:webauthn_registrations, :authentication_mode)
main:    -> 0.0413s
main: -- remove_column(:webauthn_registrations, :passkey_eligible)
main:    -> 0.0020s
main: -- remove_column(:webauthn_registrations, :last_used_at)
main:    -> 0.0022s
main: == 20250926104422 AddPasskeyColumnsToWebauthnRegistrations: reverted (0.0521s) 
main: == [advisory_lock_connection] object_id: 156160, pg_backend_pid: 64613
ci: == [advisory_lock_connection] object_id: 156160, pg_backend_pid: 65283
ci: == 20250926104422 AddPasskeyColumnsToWebauthnRegistrations: reverting =========
ci: -- remove_column(:webauthn_registrations, :authentication_mode)
ci:    -> 0.0559s
ci: -- remove_column(:webauthn_registrations, :passkey_eligible)
ci:    -> 0.0021s
ci: -- remove_column(:webauthn_registrations, :last_used_at)
ci:    -> 0.0020s
ci: == 20250926104422 AddPasskeyColumnsToWebauthnRegistrations: reverted (0.0775s) 
ci: == [advisory_lock_connection] object_id: 156160, pg_backend_pid: 65283

How to set up and validate locally

  • Clone this branch & run migrations
bin/rails db:migrate
  • Open a rails console
gdk rails console
  • Create a user with a WebAuthn registration and verify model changes.
user = User.first
auth1 = user.webauthn_registrations.create!(
 name: '1Password',
 credential_xid: SecureRandom.hex,
 public_key: SecureRandom.hex,
 counter: 0
)

user.webauthn_registrations.size
user.passkeys.size

auth1.update!(authentication_mode: 'passwordless')
user.webauthn_registrations.size
user.passkeys.size

auth1.last_used_at
auth1.update_last_used_at!
auth1.last_used_at

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Hakeem Abdul-Razak

Merge request reports

Loading