Skip to content
You are reading the v0.20 documentation. Go to the latest version (v1.0)

Security

Two-factor authentication

TOTP two-factor authentication (RFC 6238) with QR enrolment, hashed recovery codes, replay protection and throttling, scaffolded by install:auth --2fa.

Last updated:

Terminal
vukem install:auth --2fa   # also works on a project that already ran install:auth
vukem migrate

The scaffold adds Fortify-style TOTP two-factor authentication on top of auth/totp (RFC 6238: SHA-1, 6 digits, 30 seconds, the authenticator-app defaults), for the HTMX starter and the Inertia React, Vue and Svelte starters:

Text
app/models/two_factor.go                 TwoFactor: the 2FA columns of users
app/accounts/two_factor.go               enable, confirm, verify, recovery codes, disable, pending login
app/controllers/account/two_factor*.go   settings page and login challenge
app/requests/two_factor.go               code, challenge and password confirmation schemas
database/migrations/*_add_two_factor_columns.go
resources/views/auth/two-factor*.html    or resources/js/pages/Auth/TwoFactor*.{tsx,vue,svelte}
routes/two_factor.go                     appended to routes.All()
tests/two_factor_test.go                 the whole flow

It also patches your Login handler so a correct password leads to the challenge, links the settings page from the dashboard, and makes social sign-in stop at the challenge too. If Login was rewritten by hand, the command fails before writing anything and prints the change to make, so 2FA is never installed without being enforced.

Flows#

FlowBehaviour
Storagethe secret and the recovery code hashes are encrypted columns keyed from APP_KEY; TwoFactor is its own model, so the secret never reaches User or shared props
Enable (/user/two-factor)a new secret, shown as a QR code (inline SVG of the otpauth:// URI, issuer APP_NAME) and a setup key. A confirmed setup is never replaced (409)
Confirm2FA is on only after a valid code; 8 recovery codes are then shown once (only hashes are stored)
Recovery codesregenerating invalidates the old ones; the page shows how many are left
Disableneeds the current password
Logina correct password for a user with 2FA does not log in: the session id is regenerated, the pending login is stored for 5 minutes and the browser goes to /two-factor-challenge
Challengean app code or a recovery code (consumed) logs in with the remember-me choice and goes to the intended URL
Replaya code whose time step is at or before the last accepted one is refused, including the confirmation code; concurrent requests with the same code have one winner
Throttlingchallenge, confirm and disable: 5 per minute per user + IP (429 with Retry-After)
Eventsfailed codes and passwords log the auth.2fa_failed security event

The totp package#

Go
secret, err := totp.GenerateSecret()                       // base32
uri := totp.URI("Vukem", user.Email, secret, totp.Options{}) // otpauth://totp/...
svg, err := totp.QR(uri, 200)                                // inline SVG

step, ok := totp.Validate(secret, code, time.Now(), lastStep, totp.Options{}) // store step on success
plain, hashes, err := totp.RecoveryCodes(8)                  // show plain once, store hashes
remaining, ok := totp.UseRecoveryCode(input, hashes)
code, err := totp.Code(secret, time.Now(), totp.Options{})   // in tests

The QR SVG is written by totp.QR itself from server data, which is why the scaffold may insert it as raw HTML. Never do that with user input.