auth: two-step sign-in (TOTP) for admins, with recovery codes
auth/totp.ts per 08 §2.4: RFC 6238 (HMAC-SHA1, 30 s, 6 digits, ±1 step) with node:crypto only, checked against the RFC's test vectors. Migration 0003: users.totp_recovery_hashes and totp_last_step. Admins turn it on from My account (the header's "Account"): scan the QR or type the key, confirm a code, get 10 recovery codes once. Login asks for the code only after the password is right (401 totp_required, no session); a wrong code is a failed login (rate limit, audit). A step signs in once (totp_last_step updated atomically) and a recovery code once (array_remove guarded by any()). Off and new codes need the password; another admin can turn it off for a lost phone, which signs that user out. Users list shows it (twoStep). Also, found by the full e2e chain on one shared database: - Linphone tokens: only the exact token issued is accepted. The last base64 character can carry ignored bits, so another spelling decoded to the same signed token. - Test scripts no longer assume a fresh system: m3-t3 picks Dan in the test box, m8-check dials whatever slot the park took (findslot = next), m10-t4 uses the admin's existing extension, m11-t1 clears the public IP on its own first (local nets are in the transports while one is set, and transports change only on a restart). e2e m11-t1-totp; the full chain M1-M11 (33 scripts) passes. Task: M11-T1
This commit is contained in:
1 parent
65e777ab41
commit
8c750c67ca
31 files changed
+4842
-43
No files matched your search
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "users" ADD COLUMN "totp_recovery_hashes" text[] DEFAULT '{}' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "users" ADD COLUMN "totp_last_step" bigint;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -22,6 +22,13 @@
|
||||
"when": 1792371506100,
|
||||
"tag": "0002_queue_calls",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1792439261267,
|
||||
"tag": "0003_users_totp",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -57,6 +57,10 @@ export const users = pgTable('users', {
|
||||
role: userRole().notNull().default('user'),
|
||||
isActive: boolean().notNull().default(true),
|
||||
totpSecretEnc: text(),
|
||||
/** sha256 of the unused recovery codes (08 §2.4). */
|
||||
totpRecoveryHashes: emptyTextArray(),
|
||||
/** The last 30 s step a code was accepted for: the same code can't sign in twice. */
|
||||
totpLastStep: bigint({ mode: 'number' }),
|
||||
mustChangePassword: boolean().notNull().default(false),
|
||||
lastLoginAt: tstz(),
|
||||
...timestamps,
|
||||
|
||||
Reference in new issue
Block a user