Repository navigation
feat(admin): disable/remove admins, TOTP QR code, audited sign-ins and failed 2FA - #37
Merged
Merged
Conversation
…dmin database Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… last active admin); audit sign-ins, sign-outs and failed 2FA; first invite of a seeded admin is no resend Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, labels for the new audit actions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jayleaton
marked this pull request as ready for review
October 22, 2026 09:24
jayleaton
added a commit
that referenced
this pull request
Oct 22, 2026
…admin actions) - The admin auth allowlist (audited by AuthEvents) is mounted with the opt-in JSON API, behind apiOnly, since scripts need it for bearer tokens; by default cmd/admin still mounts nothing under /api/. - Crawl covers the new disable/enable/remove routes; their loading labels drop inline styles for the CSP. - Wiring tests read invite tokens from the /set-password link and call auth the way scripts do (Expo-Origin, not a browser Origin). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jayleaton
added a commit
that referenced
this pull request
Oct 22, 2026
…y server-side (#38) * feat(security): browsers only see the UI; API, secrets and errors stay server-side - JSON API (/api/auth, /api/core) and /healthz answer any browser request (Sec-Fetch-*, Origin, Referer, Accept: text/html) with a bare 404, no CORS, no 405/Allow. Admin app mounts no JSON API and no auth endpoints. - Emailed auth links for the UIs open UI pages (auth.Config.VerifyEmailPath, relative redirectTo); deep-link callbacks keep the redirecting endpoints. - UI responses carry a nonce CSP without unsafe-*, nosniff, XFO, strict-origin referrer, Permissions-Policy, COOP/CORP, no-store, HSTS on https; inline styles removed; static has no directory listing; unrouted paths are 404. - Config credentials are secrets.Secret; Reveal only in cmd (forbidigo); redacting slog handler for tokens, cookies, auth headers, emailed links. - Generic errors: plain 500 for UI panics, JSON-typed validation messages. - Admin session cookies use __Host-; locale cookies are HttpOnly. - Exposure crawl tests over every UI route in both apps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(security): close review findings (open redirect, link exception, crawler blind spot) - web safeNext rejects control characters and backslashes (/%09/evil). - Deep-link exception only for deep-link or other-origin callbacks, works without Fetch Metadata, and turns auth JSON into the bare 404. - /api is a 404 (no 307); web non-UI responses get nosniff/XFO/no-store. - Crawler checks SSE bodies (live streams cut after their first event), probes for markup injection and off-site redirects; TestNoUnescapedOutput bans templ.Raw/Unsafe* in UI sources. - Redactor covers header/value maps and struct JSON; theme cookie is secure on https; docs say templ escaping, not the CSP, is the XSS defence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(admin): reconcile the lockdown with #37 (audited auth endpoints, admin actions) - The admin auth allowlist (audited by AuthEvents) is mounted with the opt-in JSON API, behind apiOnly, since scripts need it for bearer tokens; by default cmd/admin still mounts nothing under /api/. - Crawl covers the new disable/enable/remove routes; their loading labels drop inline styles for the CSP. - Wiring tests read invite tokens from the /set-password link and call auth the way scripts do (Expo-Origin, not a browser Origin). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(admin): set invite passwords through the audited endpoint again TestAuthEvents expects the set_password sign-in in the audit trail; the helper reads the token from the /set-password link and posts it to the auth endpoint the opt-in API mounts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 22, 2026
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.
Follow-ups to #26. Admins can now be disabled, enabled and removed. The 2FA setup page shows a QR code. Admin sign-ins, sign-outs and wrong 2FA codes are audited. The bootstrap admin's first CLI invite is no longer recorded as a resend.
Summary
github.com/boombuler/barcode(pure Go, no dependencies, MIT, v1.1.0) as a single<path>. There is no external service and no image URL, and the otpauth URI and secret are never logged.failedVerificationCountandupdatedAt. AuthEvents serializes verifications within the process, so after a failure the counter that moved since the check started is its own. That is exact with one admin instance, which is the template's setup. With several instances, two simultaneous failures could be attributed to both admins. Anauth.Configevent hook would be the cleaner long-term source; this PR stays insideapps/adminandadminrepoas asked.Evidence
Before (#26): there was no way to disable or remove an admin, and no QR code (manual key only). Sign-ins and 2FA failures weren't audited. The bootstrap admin's first CLI invite was logged with
resent: true.After:
Real servers: web on :3200 and admin on :3201 over one temporary Postgres container (since removed), with
ADMIN_REQUIRE_2FA=true, driven by headless Chromium. The QR code was decoded withzbarimgfrom the rendered SVG, and the code entered came from the decoded secret:The audit log then read, newest first: Removed admin, Enabled admin, Disabled admin, Signed in (eve, password + two-factor), Invited admin, Signed in, Failed two-factor code (1 in a row), Signed out, Signed in, Invited admin by
cliwith no "resent", Created first admin. The server log contained no otpauth or secret, and request paths show/api/auth/reset-password/{token}.The 7 screenshots, including the QR enrolment page, the admins page with eve disabled, and the audit log in en and ja, are on the dev box at
~/Documents/spark-work/zig-dsv41-scratch/admin-shots/followups/. They are not uploaded to GitHub.Local checks (the COMPUTE RULE limits them to gofmt, vet and plain
go testof the touched packages):The new DB-backed tests are
TestAdminManagement,TestAuthEvents,TestAdminsRemoveandTestAdminsFailedSince.TestAuthEventscovers JSON sign-in → wrong TOTP → right TOTP → sign-out, and asserts the exact audit trail.Full
make ci(lint,-race, coverage floors, probes): see this PR's CI run.Merge Danger
Door: two-way.
There are no migrations; the 2FA counter query is read-only. Removing an admin deletes their row in the admin database, but that is an explicit, confirmed and audited admin action, not something deploying this PR does. A code revert removes the features.
Blast Radius: service.
Only
apps/adminandpackages/database/adminrepochange, plus one new Go dependency (boombuler/barcode). The web app is untouched. The audit log gains a few sign-in and sign-out rows per admin session.🤖 Generated with Claude Code