feat(account): profile, security and TOTP two-factor authentication - #208
Open
sineld wants to merge 4 commits into
Open
feat(account): profile, security and TOTP two-factor authentication#208sineld wants to merge 4 commits into
sineld wants to merge 4 commits into
Conversation
Adds the API layer for the account and MFA endpoints, plus the pure helpers the UI branches on. Every security decision stays on the server. What lives here is presentation logic and the detection of the one signal the login flow has to branch on: a demand for a second factor is a successful password check, not a failure, and reporting it as "login failed" would send the user to reset a password that is working. `fetchMfaChallenge` signs with the long-term key the user just typed, because at login time there is no session to sign with. That signature requirement is also what keeps the endpoint from being an enumeration oracle. A server without the endpoint, or any transport failure, is reported as "not required" rather than as an error: `AssumeRole` fails closed on its own, so this cannot become a bypass. The QR arrives as a server-rendered SVG and is exposed as a data URI, not injected as markup — no QR library is added, and a server-generated SVG never becomes a same-origin script sink.
The account menu previously opened on an avatar with no name and hid password management from administrators, because the backend rejected the call it made. Both are fixed: the menu now names the signed-in identity and its authority, and links to Profile and Security. Adds `/account` and `/account/security`, dialogs for password change, two-factor setup, disable and recovery-code regeneration, and the second-factor step in the login flow. Design notes: * Setup is one dialog with internal steps, not a chain of dialogs: it is one decision, and a parent closing underneath the recovery codes would orphan the only copy that will ever exist. * Recovery codes cannot be dismissed until copied or downloaded. The server keeps only hashes and cannot show them again. * Turning the factor off asks for the code *and* the password. A code alone would let a session someone walked away from strip the protection. * Controls are gated on what the server reports as possible, and an unavailable feature explains itself instead of showing a dead button — a root identity says its credentials come from the server environment. * A failed read never renders as an empty profile; passive metadata is a definition list rather than a grid of cards. The long-term credentials needed for the second call stay in component state; only the resulting STS session is persisted, and no TOTP secret reaches browser storage. Replaces `components/user/change-password.tsx`, which called `add-user` and so rewrote the target's status and dropped its policy field.
`components/object/tiff-viewer.tsx` was not Prettier-clean on `main`, so `pnpm format:check` — a required gate in AGENTS.md — failed for every branch regardless of its contents. Whitespace only; no behaviour change. Unrelated to the account and two-factor work in this branch, and separated into its own commit so it can be cherry-picked or dropped independently.
UI-027 and UI-028, per the audit register's own convention: the account menu named neither the identity nor its authority and hid password management from administrators, and a recovery-code set is the only copy that will ever exist, so its dialog must not be freely dismissable. Evidence key `account-2fa-29` is live runtime captures rather than static fixtures. The note states what it does not cover — a mobile viewport and a `main`-branch before-capture — so the source tests stay authoritative for those two rather than being implied by the key.
sineld
marked this pull request as ready for review
August 25, 2026 18:54
|
+1 |
sineld
force-pushed
the
feat/admin-account-2fa
branch
from
August 25, 2026 19:29
8f9f3b3 to
0bca22b
Compare
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.
Pull Request
Description
Makes the account area meaningful and adds TOTP two-factor authentication to the Console.
Before this, the account menu opened on an avatar with no name, and password management was hidden from administrators — because the backend rejected the call it made (
add-userrefuses the root access key). Both are fixed./account(profile) and/account/security(password + two-factor) pages.Merge order
This PR must merge after rustfs/rustfs#6596. The endpoints it calls do not exist
until then, so merging this first ships a console whose Profile and Security pages
fail against every released server.
2a and 2b are independent of each other and can land in either order, or together.
Kept as a draft until 1 lands.
Type of Change
The bug fix:
components/user/change-password.tsxchanged a password by re-POSTing the whole user throughadd-user, which rewrotestatusand dropped the policy field — a password reset that silently re-enabled a disabled account. It is replaced byPOST /account/password, which touches only the secret.Design notes for reviewers
Followed
skills/rustfs-console-design-guide/SKILL.md; the decisions worth checking:Security-relevant:
data:URI via<img>, never injected as markup.AssumeRolefails closed on its own, so this cannot become a bypass.Testing
New suites:
tests/lib/mfa.test.ts(16 tests over code-shape detection, the MFA-required signal including cyclic error objects, and QR/secret formatting) andtests/lib/account-surface.test.js(12 tests pinning the state handling, the no-nested-dialog rule, the no-secret-in-storage rule, and that the account pages stay reachable for every authenticated identity).Two existing tests were updated rather than deleted, because this PR deliberately reverses what they asserted:
iam-remaining-safety.test.jsasserted the password dialog does not ask for the current secret — correct foradd-user, wrong for/account/password, where it is the proof-of-knowledge step. Retargeted at the new dialog and now asserts the stronger contract, plus thatadd-user/createUsercannot come back.ui-layout-source.test.jspinned the exact avatar markup. Same intent (artwork smaller than its trigger, visible inset), updated for the addedshrink-0.Checklist
input-otpwas already inpackage.jsonand unused; the QR needs no library because the server renders itRelated Issues
N/A — no tracking issue exists yet.
Screenshots
Captured against a real stack: a debug build of rustfs/rustfs#6596 with
RUSTFS_IAM_MASTER_KEYset, pluspnpm devon this branch, signed in as the rootidentity
sinan. Every state below is a genuine server response, not a fixture.Account menu — before / after
Before, the menu opened on an avatar with no name, and password management was
hidden from administrators because the backend rejected the call it made. After,
the trigger and the menu both name the identity and its authority, and link to
Profile and Security.
/account/security— two-factor off → onRoot reports its password as immutable (it comes from
RUSTFS_ACCESS_KEY) whileenrollment stays available. That split is the whole point: the default deployment
signs into the console as root, so refusing enrollment there would leave the one
login this feature exists for unprotected.
Setup dialog
The QR is rendered by the server and embedded as a
data:URI — no QR library inthe console. Captured at a narrow viewport (628 CSS px), which is why the dialog
fills the width.
Recovery codes — the dismissal guard
Regenerating asks for a second factor first. The new set arrives with Done
disabled until the codes are copied or downloaded, because the server keeps only
hashes and cannot show them again.
/accountprofileLogin second-factor step
The password was accepted and the server asked for a code — the UI shows the
second-factor step, not "Login Failed". This is the branch the whole
LoginOutcometype exists for.Completing it with a real TOTP code signs in and advances the server-side
Last usedstamp, visible here after the login above:Two gaps I am not papering over:
narrow CSS viewport —
resize_windowreported success whilewindow.innerWidthstayed put. The setup-dialog capture happens to be at 628 CSS px, which is the
only sub-
mdevidence here. The responsive rules are covered by source tests,not by a capture.
server on
main; the after-state above is the honest half.Images live on an orphan branch of the fork
(
ui-evidence/account-2fa)so no binaries enter this PR's diff or the upstream history.
Additional Notes
Two things a reviewer should know:
pnpm format:checkwas already failing onmain.components/object/tiff-viewer.tsxwas not Prettier-clean in the base branch, so the gate failed for every branch regardless of contents. Fixed in its own whitespace-only commit (style: format tiff-viewer with Prettier) so it can be cherry-picked or dropped independently of the feature.{account}) to match this repo's i18next configuration (prefix: "{"), not the i18next default — worth a glance, since double braces would have rendered literally.