Repository navigation
feat(2fa): sign in with an authenticator app, with recovery codes - #277
Merged
Merged
Conversation
- two-fa/check no longer starts a session from the identity alone, which let
the emailed/SMS code stand in for the password and revealed which accounts
have 2FA on. It now always answers {twoFaSession: null, isTwoFaEnabled: false}
so older consoles fall through to auth/login, which already starts the
session after the password check. createTwoFaSessionIfEnabled is deprecated.
- Store 2FA sessions with a 600 second TTL. EX was being given an absolute
timestamp, so sessions lived for decades.
- Invalidate a 2FA session after 5 wrong codes, counted per session so
resending a code does not reset the count, and compare codes with
hash_equals.
- Generate verification codes with random_int instead of mt_rand.
Adds authenticator apps (TOTP, RFC 6238) as a 2FA method, next to email and SMS. Works with Google Authenticator, Authy, 1Password and similar apps. - Setup: POST users/two-fa/authenticator/setup (current password) returns a secret, an otpauth:// URL and a QR code. confirm checks a code from the app, makes it the user's 2FA method and returns 8 one-time recovery codes. disable and recovery-codes also need the current password. - Sign-in: when the method is authenticator_app, two-fa/validate sends nothing and returns method "authenticator_app". two-fa/verify accepts a code from the app (one time step either side, each code usable once) or a recovery code. Wrong codes count towards the existing 5-attempt lockout. two-fa/resend sends a code by email (SMS without an email) as a fallback. - Storage: the secret is encrypted with the app key; recovery codes are stored as keyed SHA-256 hashes. Neither is ever returned again or logged. - Enable, disable, recovery code use, successful sign-ins and lockouts are written to the `auth` activity log. - saveTwoFactorSettings refuses authenticator_app until an app is set up. - New Fleetbase\Support\Barcode helper, which owns milon/barcode. QR codes are drawn as a compact SVG on a white background with a quiet zone, so they scan on dark themes too. - New dependencies: pragmarx/google2fa ^8.0 and milon/barcode ^10.0 (already installed everywhere through Fleet-Ops). - Tests: the shared test container now provides a recording activity logger and a test encrypter. Closes #163
This was referenced Sep 27, 2026
Merged
roncodes
changed the base branch from
fix/2fa-login-hardening
to
release/v1.6.65
September 28, 2026 03:28
6 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #163. Stacked on #272 (2FA hardening): it builds on that PR's sign-in flow and attempt limit. Merge #272 first and this PR will retarget to
main. Console side: fleetbase/fleetbase#686.Why
The ticket asks for Authy-style authenticator 2FA next to email and SMS. Twilio has retired the Authy API, so this uses standard authenticator-app codes (TOTP, RFC 6238). They work with Authy as well as Google Authenticator, Microsoft Authenticator and 1Password, with no per-verification cost and no vendor dependency.
What changed
Setup (all in the
usersgroup; the account's own data only):POST users/two-fa/authenticator/setup→ secret,otpauth://URL, QR codePOST users/two-fa/authenticator/confirm→ 8 recovery codes, shown oncePOST users/two-fa/authenticator/disablePOST users/two-fa/recovery-codes→ new codesGET users/two-fa/authenticator→{enabled, confirmed_at, recovery_codes_remaining}saveTwoFactorSettingsrefusesauthenticator_appuntil an app is set up.Sign-in
authenticator_app,two-fa/validatesends nothing and returnsmethod: "authenticator_app". The client token points at the user server side (Redis), so it doesn't expose who it's for.two-fa/verifyaccepts a code from the app, allowing one 30-second step either side for clock drift. Each code works once (the last used step is stored). It also accepts a recovery code, once each; dashes, spaces and case are ignored.two-fa/resendsends a code by email (SMS if there's no email) as the fallback the ticket asks for. It returns the newmethodso the console can switch.Storage and logging
authactivity log:authenticator_enabled,authenticator_disabled,recovery_codes_regenerated,recovery_code_used,two_factor_verified(with the method), andtwo_factor_locked.QR codes and dependencies
Fleetbase\Support\Barcodehelper. It ownsmilon/barcode, so extensions can move to it later instead of calling milon directly.qrCodeSvg()draws a compact SVG (one path, about 9 KB for anotpauth://URL, versus 59 KB from milon's own SVG). It has a white background and a 4-module quiet zone, so it scans on the console's dark theme. milon's SVG is transparent with no border.pragmarx/google2fa ^8.0, andmilon/barcode ^10.0, which every install already has through Fleet-Ops. Nobacon/bacon-qr-code.Tests
illuminate/encryption.Heads-up
composer updateto installpragmarx/google2fa.Verification
Barcoderenders exact runs, with the quiet zone and errors covered;otpauth://QR decoded correctly with Chromium'sBarcodeDetector, drawn over the dark console background.method: authenticator_app, and 0 codes are sent;