fix(security): prevent TOTP reuse (#12616)

# Which Problems Are Solved

TOTP codes could be reused, violating [RFC 6238, section
5.2](https://datatracker.ietf.org/doc/html/rfc6238#section-5.2).

# How the Problems Are Solved

Transmitted codes are now recorded in each success event. As the codes
amount to secrets, they are not stored verbatim but as a randomly salted
HMAC-SHA256 (`crypto.HMACValue`). Each value gets a fresh random salt,
so an equal code always produces a different stored value and two
entries can never be recognized as being derived from the same code.

This is deliberately not a replacement for proper secret storage: with
the salt stored alongside the hash and a 6 digit payload, anyone with
event access can brute-force the code in milliseconds. That is
acceptable here because a code is worthless once its window has passed.
The `HMACValue` doc comment states the same.

TOTP codes are low-entropy (6 digits), pseudo random codes. Therefore it
is possible for the same code to be generated more than once over the
lifetime of a user. In order to prevent the (unlikely) case where users
login with large time intervalls and a generated code may be equal to
the one last used a long time ago, we only check for codes used within
the recent period window.

A simpler implementation would be simply storing the latest consumed
step, but the TOTP library does not return this value. Because of the
allowed skew there are always 3 steps within the validation window, so
we can't calculate the current step based on time alone. The retained
history spans one period more than the validation window: validity is
decided by the app server's clock, while the stored events carry the
database clock, and the extra period absorbs the drift between them.

**Note:** a reused code pushes a distinct
`user.human.mfa.otp.check.reused` event, not a failed check event. A
replay therefore stays observable through the Events API without
counting towards the lockout policy. Otherwise anyone who captured a
code could replay it to lock the user out.

# Additional Changes

- Read only the latest events in recheck query

# Additional Context

- Closes https://github.com/zitadel/zitadel/issues/6452

---------

Co-authored-by: Livio Spring <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: muhlemmer <[email protected]>
This commit is contained in:
Tim Möhlmann
2026-08-27 07:17:39 +02:00
committed by GitHub
co-authored by Livio Spring Copilot Autofix powered by AI copilot-swe-agent[bot] <198982749+[email protected]> muhlemmer
parent c9d747d407
commit 6128aeaa89
56 changed files with 850 additions and 8 deletions
+2
View File
@@ -177,6 +177,7 @@ Errors:
NotExisting: "المصادقة متعددة العوامل OTP (كلمة مرور لمرة واحدة) غير موجودة"
NotReady: "المصادقة متعددة العوامل OTP (كلمة مرور لمرة واحدة) ليست جاهزة"
InvalidCode: "رمز غير صالح"
Reused: "تم استخدام هذا الرمز بالفعل. يرجى انتظار رمز جديد"
U2F:
NotExisting: "U2F غير موجود"
Passwordless:
@@ -763,6 +764,7 @@ EventTypes:
check:
succeeded: "نجح فحص OTP للمصادقة متعددة العوامل"
failed: "فشل فحص OTP للمصادقة متعددة العوامل"
reused: "أعيد استخدام رمز OTP للمصادقة متعددة العوامل"
sms:
added: "تمت إضافة OTP SMS للمصادقة متعددة العوامل"
removed: "تمت إزالة OTP SMS للمصادقة متعددة العوامل"
+2
View File
@@ -176,6 +176,7 @@ Errors:
NotExisting: "Многофакторният OTP (OneTimePassword) не съществува"
NotReady: "Многофакторният OTP (OneTimePassword) не е готов"
InvalidCode: "Невалиден код"
Reused: "Този код вече е използван. Моля, изчакайте нов код"
U2F:
NotExisting: "U2F не съществува"
Passwordless:
@@ -763,6 +764,7 @@ EventTypes:
check:
succeeded: "Многофакторната OTP проверка е успешна"
failed: "Многофакторната OTP проверка е неуспешна"
reused: "Многофакторният OTP код е използван повторно"
sms:
added: "Добавен Многофакторната OTP SMS"
removed: "Премахнато многофакторно OTP SMS"
+2
View File
@@ -176,6 +176,7 @@ Errors:
NotExisting: "Vícefaktorové OTP (OneTimePassword) neexistuje"
NotReady: "Vícefaktorové OTP (OneTimePassword) není připraveno"
InvalidCode: "Neplatný kód"
Reused: "Tento kód již byl použit. Počkejte prosím na nový kód"
U2F:
NotExisting: "U2F neexistuje"
Passwordless:
@@ -765,6 +766,7 @@ EventTypes:
check:
succeeded: "Kontrola OTP pro vícefaktorové ověření byla úspěšná"
failed: "Kontrola OTP pro vícefaktorové ověření selhala"
reused: "Kód OTP pro vícefaktorové ověření byl použit opakovaně"
sms:
added: "SMS pro vícefaktorové OTP přidána"
removed: "SMS pro vícefaktorové OTP odstraněna"
+2
View File
@@ -176,6 +176,7 @@ Errors:
NotExisting: "Multifaktor OTP (OneTimePassword) existiert nicht"
NotReady: "Multifaktor OTP (OneTimePassword) ist nicht bereit"
InvalidCode: "Code ist ungültig"
Reused: "Dieser Code wurde bereits verwendet. Bitte warten Sie auf einen neuen Code"
U2F:
NotExisting: "U2F existiert nicht"
Passwordless:
@@ -767,6 +768,7 @@ EventTypes:
check:
succeeded: "Multifaktor OTP Verifikation erfolgreich"
failed: "Multifaktor OTP Verifikation fehlgeschlagen"
reused: "Multifaktor OTP Code wiederverwendet"
sms:
added: "Multifaktor OTP SMS hinzugefügt"
removed: "Multifaktor OTP SMS entfernt"
+2
View File
@@ -178,6 +178,7 @@ Errors:
NotExisting: "Multifactor OTP (OneTimePassword) doesn't exist"
NotReady: "Multifactor OTP (OneTimePassword) isn't ready"
InvalidCode: "Invalid code"
Reused: "This code was already used. Please wait for a new code"
U2F:
NotExisting: "U2F does not exist"
Passwordless:
@@ -772,6 +773,7 @@ EventTypes:
check:
succeeded: "Multifactor OTP check succeeded"
failed: "Multifactor OTP check failed"
reused: "Multifactor OTP code reused"
sms:
added: "Multifactor OTP SMS added"
removed: "Multifactor OTP SMS removed"
+2
View File
@@ -176,6 +176,7 @@ Errors:
NotExisting: "Multifactor OTP (OneTimePassword) no existe"
NotReady: "Multifactor OTP (OneTimePassword) no está listo"
InvalidCode: "Código no válido"
Reused: "Este código ya se ha utilizado. Espera a que se genere un nuevo código"
U2F:
NotExisting: "U2F no existe"
Passwordless:
@@ -767,6 +768,7 @@ EventTypes:
check:
succeeded: "Comprobación exitosa de Multifactor OTP"
failed: "Comprobación fallida de Multifactor OTP"
reused: "Código de Multifactor OTP reutilizado"
sms:
added: "Multifactor OTP SMS añadido"
removed: "Multifactor OTP SMS elimonado"
+2
View File
@@ -176,6 +176,7 @@ Errors:
NotExisting: "OTP multifactoriel (mot de passe à usage unique) n'existe pas."
NotReady: "OTP multifactoriel (mot de passe à usage unique) n'est pas prêt."
InvalidCode: "Code invalide"
Reused: "Ce code a déjà été utilisé. Veuillez attendre un nouveau code"
U2F:
NotExisting: "L'U2F n'existe pas"
Passwordless:
@@ -765,6 +766,7 @@ EventTypes:
check:
succeeded: "Vérification de l'OTP multifactorielle réussie"
failed: "La vérification de l'OTP multifactorielle a échoué"
reused: "Code OTP multifactoriel réutilisé"
sms:
added: "Ajout de SMS OTP multifactoriels"
removed: "Suppression des SMS OTP multifactoriels"
+2
View File
@@ -176,6 +176,7 @@ Errors:
NotExisting: "A multifaktoros OTP (OneTimePassword) nem létezik"
NotReady: "A multifaktoros OTP (OneTimePassword) nincs készen"
InvalidCode: "Érvénytelen kód"
Reused: "Ezt a kódot már felhasználták. Kérjük, várjon egy új kódra"
U2F:
NotExisting: "Az U2F nem létezik"
Passwordless:
@@ -767,6 +768,7 @@ EventTypes:
check:
succeeded: "A multifaktoros OTP ellenőrzés sikeres"
failed: "A multifaktoros OTP ellenőrzés sikertelen"
reused: "A multifaktoros OTP kód újrafelhasználva"
sms:
added: "Multifaktoros OTP SMS hozzáadva"
removed: "Multifaktoros OTP SMS eltávolítva"
+2
View File
@@ -176,6 +176,7 @@ Errors:
NotExisting: "OTP multifaktor (OneTimePassword) tidak ada"
NotReady: "OTP multifaktor (OneTimePassword) belum siap"
InvalidCode: "Kode tidak valid"
Reused: "Kode ini sudah digunakan. Silakan tunggu kode baru"
U2F:
NotExisting: "U2F tidak ada"
Passwordless:
@@ -762,6 +763,7 @@ EventTypes:
check:
succeeded: "Pemeriksaan OTP multifaktor berhasil"
failed: "Pemeriksaan OTP multifaktor gagal"
reused: "Kode OTP multifaktor digunakan kembali"
sms:
added: "OTP SMS multifaktor ditambahkan"
removed: "OTP SMS multifaktor dihapus"
+2
View File
@@ -176,6 +176,7 @@ Errors:
NotExisting: "Multifattore OTP (OneTimePassword) non esistente"
NotReady: "Multifattore OTP (OneTimePassword) non è pronto"
InvalidCode: "Codice non valido"
Reused: "Questo codice è già stato utilizzato. Attendere un nuovo codice"
U2F:
NotExisting: "U2F non esistente"
Passwordless:
@@ -766,6 +767,7 @@ EventTypes:
check:
succeeded: "Controllo OTP riuscito"
failed: "Controllo OTP fallito"
reused: "Codice OTP riutilizzato"
sms:
added: "Aggiunta SMS OTP"
removed: "SMS OTP rimossa"
+2
View File
@@ -177,6 +177,7 @@ Errors:
NotExisting: "多要素OTP(ワンタイムパスワード)が存在しません"
NotReady: "多要素OTP(ワンタイムパスワード)が利用可能でありません"
InvalidCode: "無効なコードです"
Reused: "このコードはすでに使用されています。新しいコードをお待ちください"
U2F:
NotExisting: "U2Fは存在しません"
Passwordless:
@@ -768,6 +769,7 @@ EventTypes:
check:
succeeded: "MFA OTPチェックの成功"
failed: "MFA OTPチェックの失敗"
reused: "MFA OTPコードの再利用"
sms:
added: "多要素 OTP SMS を追加しました"
removed: "多要素 OTP SMS を削除しました"
+2
View File
@@ -177,6 +177,7 @@ Errors:
NotExisting: "다중 요소 OTP(일회용 비밀번호)가 존재하지 않습니다"
NotReady: "다중 요소 OTP(일회용 비밀번호)가 준비되지 않았습니다"
InvalidCode: "코드가 잘못되었습니다"
Reused: "이 코드는 이미 사용되었습니다. 새 코드를 기다려 주세요"
U2F:
NotExisting: "U2F가 존재하지 않습니다"
Passwordless:
@@ -768,6 +769,7 @@ EventTypes:
check:
succeeded: "다중인증 OTP 확인 성공"
failed: "다중인증 OTP 확인 실패"
reused: "다중인증 OTP 코드 재사용"
sms:
added: "다중인증 OTP SMS 추가됨"
removed: "다중인증 OTP SMS 삭제됨"
+2
View File
@@ -175,6 +175,7 @@ Errors:
NotExisting: "Мултифактор OTP (Еднократна Лозинка) не постои"
NotReady: "Мултифактор OTP (Еднократна Лозинка) не е подготвен"
InvalidCode: "Невалиден код"
Reused: "Овој код е веќе искористен. Ве молиме почекајте нов код"
U2F:
NotExisting: "U2F не постои"
Passwordless:
@@ -762,6 +763,7 @@ EventTypes:
check:
succeeded: "Проверката на мултифактор OTP е успешна"
failed: "Проверката на мултифактор OTP е неуспешна"
reused: "Кодот за мултифактор OTP е повторно употребен"
sms:
added: "Додадена е мултифакторна OTP SMS"
removed: "Отстранета мултифакторна OTP SMS"
+2
View File
@@ -176,6 +176,7 @@ Errors:
NotExisting: "Multifactor OTP (OneTimePassword) bestaat niet"
NotReady: "Multifactor OTP (OneTimePassword) is niet klaar"
InvalidCode: "Ongeldige code"
Reused: "Deze code is al gebruikt. Wacht op een nieuwe code"
U2F:
NotExisting: "U2F bestaat niet"
Passwordless:
@@ -767,6 +768,7 @@ EventTypes:
check:
succeeded: "Multifactor OTP controle geslaagd"
failed: "Multifactor OTP controle mislukt"
reused: "Multifactor OTP code hergebruikt"
sms:
added: "Multifactor OTP SMS toegevoegd"
removed: "Multifactor OTP SMS verwijderd"
+2
View File
@@ -176,6 +176,7 @@ Errors:
NotExisting: "Wieloskładnikowe OTP (OneTimePassword) nie istnieje"
NotReady: "Wieloskładnikowe OTP (OneTimePassword) nie jest gotowe"
InvalidCode: "Nieprawidłowy kod"
Reused: "Ten kod został już użyty. Poczekaj na nowy kod"
U2F:
NotExisting: "U2F nie istnieje"
Passwordless:
@@ -767,6 +768,7 @@ EventTypes:
check:
succeeded: "Sprawdzenie wielofaktorowego OTP zakończone powodzeniem"
failed: "Sprawdzenie wielofaktorowego OTP nie powiodło się"
reused: "Kod wielofaktorowego OTP użyty ponownie"
sms:
added: "Dodano wieloskładnikowy OTP SMS"
removed: "Usunięto wieloskładnikowy OTP SMS"
+2
View File
@@ -175,6 +175,7 @@ Errors:
NotExisting: "OTP (OneTimePassword) de autenticação multifator não existe"
NotReady: "OTP (OneTimePassword) de autenticação multifator não está pronto"
InvalidCode: "Código inválido"
Reused: "Este código já foi utilizado. Aguarde um novo código"
U2F:
NotExisting: "U2F não existe"
Passwordless:
@@ -762,6 +763,7 @@ EventTypes:
check:
succeeded: "Verificação de OTP de autenticação multifator bem-sucedida"
failed: "Verificação de OTP de autenticação multifator falhou"
reused: "Código de OTP de autenticação multifator reutilizado"
sms:
added: "Adicionado OTP SMS multifator"
removed: "OTP SMS multifator removido"
+2
View File
@@ -177,6 +177,7 @@ Errors:
NotExisting: "Multifactor OTP (Parolă Unică) nu există"
NotReady: "Multifactor OTP (Parolă Unică) nu este pregătit"
InvalidCode: "Cod invalid"
Reused: "Acest cod a fost deja utilizat. Vă rugăm să așteptați un cod nou"
U2F:
NotExisting: "U2F nu există"
Passwordless:
@@ -763,6 +764,7 @@ Errors:
check:
succeeded: "Verificarea Multifactor OTP a reușit"
failed: "Verificarea Multifactor OTP a eșuat"
reused: "Cod Multifactor OTP reutilizat"
sms:
added: "Multifactor OTP SMS adăugat"
removed: "Multifactor OTP SMS șters"
+2
View File
@@ -176,6 +176,7 @@ Errors:
NotExisting: "Мультифактор OTP (OneTimePassword) не существует"
NotReady: "Мультифактор OTP (OneTimePassword) не готов"
InvalidCode: "Неверный код"
Reused: "Этот код уже был использован. Пожалуйста, дождитесь нового кода"
U2F:
NotExisting: "Двухфакторная аутентификация не существует"
Passwordless:
@@ -756,6 +757,7 @@ EventTypes:
check:
succeeded: "Проверка мультифактора OTP прошла успешно"
failed: "Проверка мультифактора OTP не удалась"
reused: "Код мультифактора OTP использован повторно"
sms:
added: "Добавлен многофакторный OTP SMS"
removed: "Удалены многофакторные OTP SMS"
+2
View File
@@ -176,6 +176,7 @@ Errors:
NotExisting: "Tvåfaktor OTP (OneTimePassword) finns inte"
NotReady: "Tvåfaktor OTP (OneTimePassword) är inte redo"
InvalidCode: "Ogiltig kod"
Reused: "Den här koden har redan använts. Vänta på en ny kod"
U2F:
NotExisting: "U2F finns inte"
Passwordless:
@@ -767,6 +768,7 @@ EventTypes:
check:
succeeded: "Tvåfaktor OTP-kontroll lyckades"
failed: "Tvåfaktor OTP-kontroll misslyckades"
reused: "Tvåfaktor OTP-kod återanvänd"
sms:
added: "Tvåfaktor OTP SMS tillagd"
removed: "Tvåfaktor OTP SMS borttagen"
+2
View File
@@ -177,6 +177,7 @@ Errors:
NotExisting: "Çok faktörlü OTP (Tek Kullanımlık Şifre) mevcut değil"
NotReady: "Çok faktörlü OTP (Tek Kullanımlık Şifre) hazır değil"
InvalidCode: "Geçersiz kod"
Reused: "Bu kod zaten kullanıldı. Lütfen yeni bir kod bekleyin"
U2F:
NotExisting: "U2F mevcut değil"
Passwordless:
@@ -768,6 +769,7 @@ EventTypes:
check:
succeeded: "Çok faktörlü OTP kontrolü başarılı"
failed: "Çok faktörlü OTP kontrolü başarısız"
reused: "Çok faktörlü OTP kodu yeniden kullanıldı"
sms:
added: "Çok faktörlü OTP SMS eklendi"
removed: "Çok faktörlü OTP SMS kaldırıldı"
+2
View File
@@ -176,6 +176,7 @@ Errors:
NotExisting: "Багатофакторний OTP (одноразовий пароль) не існує"
NotReady: "Багатофакторний OTP (одноразовий пароль) не готовий"
InvalidCode: "Недійсний код"
Reused: "Цей код вже було використано. Будь ласка, зачекайте на новий код"
U2F:
NotExisting: "U2F не існує"
Passwordless:
@@ -762,6 +763,7 @@ EventTypes:
check:
succeeded: "Перевірка багатофакторного OTP успішна"
failed: "Перевірка багатофакторного OTP не вдалася"
reused: "Код багатофакторного OTP використано повторно"
sms:
added: "Багатофакторний OTP SMS доданий"
removed: "Багатофакторний OTP SMS видалений"
+2
View File
@@ -177,6 +177,7 @@ Errors:
NotExisting: "OTP (一次性密码) 不存在"
NotReady: "OTP (一次性密码) 还没准备好"
InvalidCode: "无效的验证码"
Reused: "此验证码已被使用。请等待新的验证码"
U2F:
NotExisting: "U2F 不存在"
Passwordless:
@@ -768,6 +769,7 @@ EventTypes:
check:
succeeded: "验证 MFA OTP 成功"
failed: "验证 MFA OTP 失败"
reused: "重复使用 MFA OTP 验证码"
sms:
added: "添加了多因素 OTP 短信"
removed: "删除了多因素 OTP 短信"