Two-factor auth
This pack adds two-factor authentication to your app. Signed-in users turn it on from their security settings, and once it is on, signing in takes a second step after the password.
- Authenticator enrollment by QR code or a secret entered by hand, confirmed with a six-digit code.
- One-time recovery codes for signing in when the authenticator is unavailable, which the user can regenerate.
- A rate-limited login challenge that accepts either a TOTP code or a recovery code.
- An email to the account owner whenever two-factor is turned on, turned off, or its recovery codes change.
Apply the two-factor pack with Flow. Start a new session in your selected coding agent and execute the following slash command inside it.
After applying, Flow will make the following changes to your app.
-
Establish a Security page to host the controls and back the modals, wherever your app is missing one.
-
Land the backend as shipped, from the migration and model mixin through the validators, controllers, routes, and the change-notification mailer.
-
Wire the modal runtime, then restyle the enrollment, recovery-codes, and challenge screens to your design system.
-
Add the challenge branch to your login, and to social sign-in when you already have it.
-
Run the tests, then walk enrollment, the recovery codes, and the login challenge and show you the result.
Apply the two-factor pack to your app by hand by working through the steps below in order. Each one builds on the last, taking your app from password-only auth to authenticator enrollment, recovery codes, and a verification step at login.
-
Confirm your auth foundation
This pack extends the auth pack. Confirm all three before continuing, and apply the auth pack first, in a fresh session, if any is missing.
- Your User model composes
withAuthFinder(...)andwithManagedEmail(), so@adonisplus/personais installed. This pack addswithTotpManagement()to that chain. SessionController.storeverifies credentials and logs the user in withauth.use('web'). This pack inserts the challenge branch into it.- A web session guard and the
authmiddleware are registered, and thesession.createroute resolves.
- Your User model composes
Flow adapts the pack to your app, so the exact set of created and edited files depends on what you already have. This is the shape of an apply onto the React starter kit with the auth pack in place.
Configuration
The withTotpManagement() mixin on the User model backs the whole feature. Pass an options object to change its defaults.
issuer
The name shown next to the code in the user's authenticator app, normally your application or company name. Defaults to AdonisJS App.
window
How many 30-second steps on either side of the current one still accept a code. A window of 1 accepts the previous, current, and next period, about 90 seconds. Defaults to 1.
recoveryCodesCount
How many single-use recovery codes each generation produces. Defaults to 10.
withTotpManagement(encryption, hash, { issuer: 'ACME', window: 2 })
Secrets are encrypted, recovery codes are hashed
The confirmed TOTP secret is encrypted at rest and each recovery code is stored only as a hash. A leaked database row cannot be turned back into a working secret or a usable code.
Enrolling takes two steps
Enabling first creates a pending secret and shows the QR code. Two-factor is not active until the user confirms it with a code from their authenticator. Until that first code lands, nothing about the account has changed.
Recovery codes are shown once
The codes appear a single time, right after the user enables two-factor or regenerates them. The screen cannot be reopened, so a refresh or a direct visit shows nothing. Regenerating replaces the whole set and retires the old codes.
A used code cannot be replayed
Each accepted code's time step is recorded, so the same code cannot be used a second time inside its window.
The login challenge is rate limited
Failed challenge attempts run through two limiter keys, one scoped per IP and a stricter one per IP-and-user that adds a 20-minute block after five failures. Both run through penalize, so the budget is spent only when a code is wrong and a correct code is always free.
const ipKey = `two_factor_${request.ip()}`
const userKey = `two_factor_${request.ip()}_${challenge.userId}`
const challengeLimiter = limiter.multi([
{ duration: '1 min', requests: 10, key: ipKey },
{ duration: '1 min', requests: 5, blockDuration: '20 mins', key: userKey },
])
Every change emails the account owner
Turning two-factor on or off, or regenerating the recovery codes, sends the account owner an email out of band. If a compromised session quietly strips two-factor, the change is at least visible to the real owner.
1.0.0
Initial release.