docs(spec-005): record the TOTP-step render fix
Add §11 (the bug, the write-free IssueTOTP seam, the rendered TOTP page, the tightened tests) and bump the status revision to 2026-10-15d. Note the two builder choices: the secret is shown as text rather than a QR image, and `pendingGUID` now means pending AND in-window (409 on an expired window).
This commit is contained in:
parent
7c1826e115
commit
820dc005b3
1 changed files with 34 additions and 1 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Status** | **Implemented** (2026-10-13) — see §7; `build`/`vet`/`test`/`-race` green · static binary · live-smoked · **rev 2026-10-14 (D-041): TOTP-only setup** · **rev 2026-10-15 (D-025/D-043): per-request tunnel gate — superseded** · **rev 2026-10-15b (D-044): the setup code + the first-run boilerplate** · **rev 2026-10-15c (D-045): the gate is the `setup` toggle; the code is optional; the tunnel gate retired** — see §9/§10 |
|
||||
| **Status** | **Implemented** (2026-10-13) — see §7; `build`/`vet`/`test`/`-race` green · static binary · live-smoked · **rev 2026-10-14 (D-041): TOTP-only setup** · **rev 2026-10-15 (D-025/D-043): per-request tunnel gate — superseded** · **rev 2026-10-15b (D-044): the setup code + the first-run boilerplate** · **rev 2026-10-15c (D-045): the gate is the `setup` toggle; the code is optional; the tunnel gate retired** · **rev 2026-10-15d: the TOTP step renders the secret + verify form (was the enroll flow's JSON)** — see §9/§10/§11 |
|
||||
| **Created** | 2026-10-13 |
|
||||
| **Author** | OpenArchitect |
|
||||
| **Target milestone** | **M0** |
|
||||
|
|
@ -242,3 +242,36 @@ code-less wizard, the code form → unlock → wizard, and a full bootstrap: `do
|
|||
| **D-2** | "No admin" (for the locked predicate) = **no user row at all**, latched at startup — not "no ACTIVE admin". A pending admin is therefore *productive* (enrolls via `/{GUID}`), never locked. | *(builder)* |
|
||||
| **D-3** | The `unlock` action when no code is required is a no-op **303 → /setup** rather than a 404. | *(builder — a stale bookmark works)* |
|
||||
|
||||
---
|
||||
|
||||
## 11. Fix — the TOTP step renders the wizard page (2026-10-15d)
|
||||
|
||||
**Bug:** `POST /setup?do=setup&action=admin.totp.begin` answered with the **enroll flow's
|
||||
JSON** (`{"otpauth_url":…}`, `Content-Type: application/json`) — `setup.Actions.TOTPBegin`
|
||||
delegated to `flow.Actions.TOTPBegin`, which is `webauthn.js`'s **JSON API**. The wizard is a
|
||||
**server-rendered, no-JS** flow, so the browser showed raw JSON and stalled at step 2 (§4's
|
||||
"TOTP enrollment" step rendered no secret at all).
|
||||
|
||||
**Fix (the enroll flow's JSON contract is kept intact — `webauthn.js` depends on it):**
|
||||
|
||||
- `internal/core/auth/flow` — the issue step is extracted into a **write-free** seam,
|
||||
`Actions.IssueTOTP(ctx, guid) (string, error)` (issues + persists the secret, returns the
|
||||
otpauth URL, writes nothing). `TOTPBegin` now calls it and keeps its JSON contract
|
||||
(`writeJSON(w, 200, {"otpauth_url": url})`).
|
||||
- `internal/core/setup` — `Enroller` gains the write-free `IssueTOTP` and **drops `TOTPBegin`**
|
||||
(so the wizard's seam no longer exposes a JSON-writing method at all); `Actions.TOTPBegin`
|
||||
now **renders an HTML page** (`text/html`, inside the shell, no inline scripts) showing the
|
||||
otpauth URL, its human-readable secret for manual entry, **and** the verify form (posting to
|
||||
`admin.totp.verify`, carrying the pre-session CSRF token, A15).
|
||||
- `internal/core/setup/render.go` — `totpPage` + `secretFromOtpauth`.
|
||||
- **Tests** — `internal/core/server/setup_wizard_test.go` now asserts the `begin` response is
|
||||
`text/html` and carries the issued secret and the verify form (not merely `200`); the
|
||||
end-to-end path (begin → real code → `303`) is kept. `enroll_login_test.go` pins the
|
||||
enroll flow's JSON contract (`application/json` + `"otpauth_url"`).
|
||||
|
||||
| # | Builder choice | Ruling |
|
||||
|---|---|---|
|
||||
| **D-1** | The secret is shown as **text** (the otpauth URL plus the base32 secret extracted from it) rather than a QR **image**: the wizard is no-JS and a QR would pull in an image/barcode renderer for no gain. | *(builder — "human-readable secret and/or QR")* |
|
||||
| **D-2** | `pendingGUID` now means **pending AND in-window** (its doc already read "still awaits enrollment"), so an expired window refuses the step (409) instead of letting `IssueTOTP` surface as a 500. `TOTPVerify` on an expired window answers 409 rather than the flow's 404 — a refusal either way. | *(builder)*
|
||||
| **D-3** | `Enroller.TOTPBegin` is **removed** — the fix left it unused by the wizard, and keeping a JSON-writing method on a no-JS seam is the coupling that caused the bug. `flow.Actions.TOTPBegin` (and its JSON contract for `webauthn.js`) is unchanged. | *(builder — dead code after the fix; trivially reversible)*
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue