TOTP codes rejected as wrong
User insists code is correct, Kratos says invalid
User enters a 6-digit code from their authenticator app. Kratos returns "invalid code." User confirms the code they're entering. Yet rejected.
Cause 1: Clock skew
TOTP is time-based. Codes are valid for 30 seconds, with a 1-window grace each side (so 90s total tolerance).
If the user's device clock is more than ~30s off from server clock, codes generated by the device don't match.
Check user clock
Ask user to verify time settings on their phone:
- iOS: Settings → General → Date & Time → Set automatically.
- Android: Settings → System → Date & time → Automatic.
Check server clock
podman exec ciam-kratos date
# Should be very close to current UTC.
# If drifted:
ssh prod 'sudo systemctl restart systemd-timesyncd'
ssh prod 'timedatectl status'See troubleshooting/clock-skew.mdx.
Cause 2: User entered code from wrong account
Authenticator apps store many TOTP secrets. User accidentally entered the code from another service.
Verify: the entry in the authenticator app shows "[Your App Name]"? Yes → correct. Otherwise, they're using the wrong one.
Cause 3: Old/replaced secret
User unenrolled MFA and re-enrolled. The authenticator app still has the OLD entry, the new entry was added but they may be tapping the wrong one.
Fix: in their authenticator, delete the old "[Your App]" entry, keep only the new one.
Cause 4: Wrong issuer / label in authenticator
If your service rebranded, the entry label might be stale. Visually confusing. Tell user to delete and re-enroll.
Cause 5: TOTP secret corruption
Rare. If the QR code wasn't scanned cleanly, the secret might be slightly off. Re-enroll.
Cause 6: Algorithm mismatch
TOTP supports SHA1, SHA256, SHA512. Most apps default to SHA1.
Kratos uses SHA1 by default. Verify:
# kratos.yml
selfservice:
methods:
totp:
config:
issuer: Your App
# algorithm defaults to SHA1If you customized to SHA256 but users enrolled before that change, their app is generating SHA1 codes which Kratos compares against SHA256 → mismatch.
Cause 7: Server-side time drift recovery
If clock skew persists beyond TOTP tolerance, fix the clock and have users re-test. Don't rely on increasing tolerance, that weakens security.
Recovery codes as escape hatch
If MFA codes are broken, user should be able to use a recovery (backup) code.
selfservice:
methods:
lookup_secret:
enabled: trueUser enrolls 10 backup codes at MFA-setup time. Login UI shows "Use a backup code" link.
If they're also missing → admin recovery (see locked-account-unlock).
Debugging server-side
podman logs ciam-kratos | grep totp | tail -50If you see totp code does not match:
- Check what time Kratos thinks it is (
datein container). - Compare to what time the user thinks it is.
- Typical: server is 60s behind real time → user codes are "from the future."