Prevent TOTP code replay within the drift window
verify_totp called ROTP without `after:`, so a captured 6-digit code stayed valid for the full ~90s drift window and could be replayed in a separate sign-in. Add a last_otp_at column, pass it as ROTP's `after:`, and persist the matched timestep on success so a code (or any earlier one) cannot be reused. Also fixes a latent bug surfaced by the new replay path: enable_totp! did `self.backup_codes = generate_backup_codes`, reassigning backup_codes to the plaintext return value (generate_backup_codes already stores the BCrypt hashes internally). That stored backup codes in plaintext and broke verification. enable_totp! is test-only today, but it is public and backup_codes is not encrypted, so this is a real footgun. Now it just calls generate_backup_codes. Rewrites the mislabeled "TOTP code cannot be reused" test to actually assert that replaying an accepted code is rejected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
3
db/schema.rb
generated
3
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_06_07_000003) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_06_11_000001) do
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.bigint "blob_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -233,6 +233,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_06_07_000003) do
|
||||
t.datetime "created_at", null: false
|
||||
t.json "custom_claims", default: {}, null: false
|
||||
t.string "email_address", null: false
|
||||
t.integer "last_otp_at"
|
||||
t.datetime "last_sign_in_at"
|
||||
t.string "name"
|
||||
t.string "password_digest", null: false
|
||||
|
||||
Reference in New Issue
Block a user