Repository navigation
feat(account): optional two-factor sign-in and revoked-session handling (#615) - #617
Merged
Merged
Conversation
…ng (#615) Client half of VoltiusApp/server#104 and #105. Two-factor sign-in - Settings > Account gets a two-factor row on servers that offer it: password, QR code and key, first code, then the recovery codes with copy and save-to-file. Managing it offers new recovery codes and turning it off. Changing the master password asks for a code. - Every sign-in path (first launch, lock screen, cloud sign-in, add account) falls into a code step when the server answers 428. The step takes an authenticator code or a recovery code and then runs the follow-up of the sign-in it interrupted. - Unlocking is no longer a sign-in: when the password opens the vault from this device's cached secrets and the server still honours the session, login() does not post to /auth/login. Without that a two-factor account would be asked for a code on every unlock. - login() writes nothing to the keychain until the server has accepted the sign-in, so one abandoned at the code step does not leave an account that reads as local. Revoked sessions - /v1/auth/refresh had three callers and none handled a refusal: the sync loop retried every five seconds for good. There is now one refreshAccessToken. A 401 is remembered, so the loop stops asking, and a toast offers to sign in again, which asks for the master password of the current account and nothing else. - A 401 for a refresh token that was replaced while the request was in flight is retried with the new one. The server announces a version bump before it answers the request that caused it, so enabling two-factor made this device report itself signed out. signInServerAccount and the re-auth branch of login() posted the login separately with different error mapping; both go through serverLogin.
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 #615. Client half of VoltiusApp/server#104 (refresh token revocation) and VoltiusApp/server#105 (TOTP + recovery codes), both merged on the server.
Two-factor sign-in is opt-in per account, off by default, free on every tier, and identical on a self-hosted server. Local accounts are not concerned.
Screens, captured from the live build: https://claude.ai/artifact/WzX7eMmvtm6tQq82XaKPs5
Two-factor
two_factorin/v1/meta(or when the account already has it on, so it can always be turned off).428 TWO_FACTOR_REQUIRED. It takes an authenticator code or a recovery code, names a refusal (wrong code, locked for 15 minutes, server cannot check codes, sign-in took too long) and then runs the follow-up of the sign-in it interrupted.login()used to post/auth/loginon every password unlock of a cloud vault. When the password opens the vault from this device's cached secrets and the server still honours the session, it now only refreshes. Without that, an enrolled account would be asked for a code at every unlock.login()writes nothing to the keychain until the server accepts the sign-in. It used to store the vault key, the password and a healedmode: "local"first, so a sign-in left at the code step would have read as a local account on the next launch.Revoked sessions
The server can now end a session (password change, two-factor on or off, log out everywhere), and
/v1/auth/refreshanswers 401 for it./v1/auth/refreshhad three callers (authTokens,sync,account) and none handled a refusal: the sync loop retried every five seconds indefinitely. They share onerefreshAccessTokennow.reauthpurpose) and goes throughlogin(), so a password changed on another device is handled the way the lock screen already handles it.Refactor
signInServerAccountand the re-auth branch oflogin()each posted the login with their own error mapping. Both go throughserverLogin, which hands the session to a continuation, immediately or after the code step.Checks
tsc --noEmitclean. Full suite: 811 files, 6421 tests passed before the last two fixes; the 44 affected files (418 tests) re-run after them.mainwithTOTP_ENC_KEY: enrol, wrong code, recovery codes, regenerate, lock and unlock (no code asked), log out everywhere from a second session → toast → sign in again → wrong code → recovery code → sync resumes, sign out → first-launch sign-in with a code.Not done
/dev/kvm). The screens are the shared components; the code field setsinputMode="numeric"andautoComplete="one-time-code". Needs a pass on a phone.qrcode-generator2.0.4 (MIT, no dependencies) for the QR code.Do not release a client with this before the server carrying #104/#105 is deployed with
TOTP_ENC_KEYset and backed up. Against an older server nothing changes: the row stays hidden and sign-in is as before.