SCRUM-258 (backend) — onboarding honours the MFA policy¶
Security fix, found while designing the onboarding screens (SCRUM-258).
The bug (on staging and the deployed host before this)¶
POST /admin-auth/onboard redeemed an invite or password-reset link and then issued a
full session with its own copy of the login success path, skipping the MFA policy:
1. an invited admin was signed in without ever enrolling TOTP (D5 requires it);
2. a reset link for a user with a confirmed TOTP signed them in with no second
factor — the link alone was enough.
The fix¶
The post-password policy is one function, completeAuthentication (session.go), used
by both login and onboarding: root → session; confirmed TOTP → {mfa_required,
mfa_ticket}; admin without a factor → {mfa_enrollment_required, mfa_ticket};
otherwise a session. The link is consumed and the password set before the challenge;
the session comes from mfa/verify or mfa/enroll + confirm as for login. The
redemption transaction returns the root flag and factor state, so no second lookup is
needed. Onboarding outcomes are counted in staff_login_total like login's. Doc 06
§13.1 states the rule.
How to verify¶
cd services/admin_auth
ADMIN_AUTH_TEST_DATABASE_URL=postgres://auth_rw:pw@127.0.0.1:5433/admin_auth_test?sslmode=disable \
go test -race -count=1 ./...
internal/server/onboard_test.go: admin invite → mfa_enrollment_required, no
Set-Cookie, then enroll + confirm signs in; live_ops/viewer invite → tokens as before;
reset link for a TOTP user → mfa_required, no Set-Cookie, verify signs in, new
password works and the old one does not; reset for a non-admin without TOTP → tokens.
Results at time of writing¶
gofmt,go vet,go test -racewith the DB (9 packages): pass.- Regression check: with onboarding forced back to "always issue a session",
TestOnboardInviteAdminRequiresEnrollmentandTestOnboardResetWithConfirmedTOTPRequiresVerifyfail; with the fix they pass.
How it was built¶
DeepSeek run scoped (Landlock) to services/admin_auth (174 s, ~26k output tokens,
reasoning effort low). Claude found the bug, reviewed the shared policy function, proved
the tests catch the bypass, and added the doc 06 note.