Skip to content

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 -race with the DB (9 packages): pass.
  • Regression check: with onboarding forced back to "always issue a session", TestOnboardInviteAdminRequiresEnrollment and TestOnboardResetWithConfirmedTOTPRequiresVerify fail; 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.