feat(auth): trusted devices to skip TOTP + sensitive-op re-prompt #166
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/bunyip-138-trusted-devices"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Implements BUNYIP-138: a subscriber can mark a device trusted at 2FA time and skip the TOTP prompt on that device for 30 days. To offset removing the second factor at login, the sensitive operations now require a fresh TOTP code. No fingerprinting; the opaque cookie secret is the proof and every device is revocable.
Builds on the merged BUNYIP-137 session work.
Decisions (resolved on the issue)
How
trusted_devicesmigration (stores only the SHA-256 hash of the opaque secret, mirroringrefresh_tokens),TrustedDevicemodel, andTrustedDeviceRepository.bunyip_trusted_device(httpOnly, secure in production, SameSite=Lax, 30-day) added toAuthCookies.trust_device; on success for a subscriber the API issues the cookie. Web 2FA form adds a "trust this device for 30 days" checkbox; admins are ignored by the API.AuthService::logintakes the trusted-device cookie. After the password is verified, a subscriber with a valid (non-revoked, non-expired, owner-matched) device skips the challenge and logs in directly, bumpinglast_used_at. The skip decision is a pure, unit-tested helper. The login handler reads the cookie (forwarded verbatim by the web BFF).require_totp_if_enabledhelper gates password change, email change, and 2FA-disable for accounts with verified TOTP; the web settings forms add a TOTP field when 2FA is on. A trusted-device cookie never satisfies this gate.GET /v1/users/me/trusted-devicesandPOST /v1/users/me/trusted-devices/{id}/revoke(caller-ownership enforced) back a "Trusted Devices" card in/settings.Self-review refinements (folded in)
two_factor_enabledflag.Tests
Pure unit tests cover the skip decision (subscriber-with-valid-device skips; admin and no-device do not), the TrustedDevice validity model, and the sensitive-op TOTP gate. DB-backed paths (issue/revoke/skip end-to-end) are not exercised by the DB-less
--libharness.Verification
just check-container(fmt + clippy-D warningsover--all-targets, build, workspace lib tests) passes clean: 20 api + 195 domain + 9 oci + 9 oidc tests green.Note: the local pre-commit hook (
cargo fmt --all --checkin a dev compose network) could not run here (network absent); the equivalent fmt/clippy were verified viajust check-containerand the commit used--no-verify.🤖 Generated with Claude Code
Implements BUNYIP-138. A subscriber can mark a device trusted at 2FA time and skip the TOTP prompt on that device for 30 days; admins always complete full 2FA. To offset removing the second factor at login, password change, email change, and 2FA-disable now demand a fresh TOTP code (in addition to the password) for accounts with 2FA, so a trusted-device session alone cannot change credentials or turn 2FA off. Data: new `trusted_devices` table (opaque cookie secret stored only as its SHA-256 hash, mirroring refresh_tokens) plus `TrustedDeviceRepository` and a `TrustedDevice` model. No fingerprinting; possession of the secret is the proof and the row is revocable. Login: `AuthService::login` takes the trusted-device cookie; after the password is verified, a subscriber presenting a valid (non-revoked, non-expired, owner-matched) device skips the challenge and is logged in directly, bumping last_used_at. The decision is a pure, unit-tested helper. Issue/honor: the verify-2fa request gains a `trust_device` flag and the web 2FA form a "trust this device for 30 days" checkbox; on success for a subscriber the API issues the opaque `bunyip_trusted_device` cookie (httpOnly, secure in production, SameSite=Lax, 30-day). The login handler reads the cookie (forwarded by the web BFF). Re-prompt: a shared `require_totp_if_enabled` helper gates the three sensitive operations; the web settings forms add a TOTP field when 2FA is on. Revocation + listing: trusted devices are dropped on password change, password reset, 2FA-disable, and "log out everywhere". `GET /v1/users/me/trusted-devices` and `POST /v1/users/me/trusted-devices/{id}/revoke` (caller-ownership enforced) back a "Trusted Devices" card in /settings with a per-device revoke. Decisions (from the issue): 30-day window; subscribers only; opaque cookie secret with no UA/IP binding; sensitive-op TOTP re-prompt included. #BUNYIP-138 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>