Skip to content

SCRUM-207 — admin-auth: onboarding (redeem an invite or reset link)

Plan ref: AA-8 (docs/11-admin-plane-plan.md). Stacked on SCRUM-206.

Behaviour

Links from SCRUM-206 are …/admin/onboard#token=<t>; the fragment never reaches a server. The admin UI reads it and POSTs it, so there is no route with the token in the path (the plan's GET /admin-auth/onboard/{token} would have put it into access logs). Both routes are public and sit on gateway_dev's strict /admin-auth/ rate-limit bucket.

Route Behaviour
POST /admin-auth/onboard/lookup {"token"} pending link → {purpose, email, name, role\|null, expires_at}; used / revoked / expired / unknown → 404 invalid_link "this link is invalid or has expired" (one answer for every cause)
POST /admin-auth/onboard {"token","password"} one transaction, link row locked FOR UPDATE and re-checked; then signs the person in exactly like login (session family, otomo_refresh cookie, access token, login body)
  • invite: creates the user (email lowercased, the invited role, created_by = the inviter), audit user.onboard. If the email became an account meanwhile → 409 already_exists and the link is burned (no replay).
  • reset: target must exist and be active; new hash, lockout cleared, every existing session revoked, audit user.password_reset.
  • password policy (400 validation_failed naming the rule): 12–128 characters; not the email or its local part; not in an embedded list of ~440 common passwords (internal/password/common.txt, case-insensitive).

How to verify

cd services/admin_auth
export ADMIN_AUTH_TEST_DATABASE_URL='postgres://USER:PASS@127.0.0.1:5433/admin_auth_test?sslmode=disable'
go vet ./... && go test -race -count=2 ./...
go test -count=1 -v -run Onboard ./internal/server/
Test Proves
TestOnboardLookup pending invite and reset return their data; used / revoked / expired / unknown → identical 404
TestOnboardInviteRedeem 200 + cookie + token carrying the invited role; user row with created_by; link used; second redeem 404; the new user can log in
TestOnboardResetRedeem old password stops working, new works, old sessions revoked, lockout cleared
TestOnboardPasswordRules 11 / 129 chars, email local part, common password → 400; link still pending
TestOnboardConcurrentRedeem 3 simultaneous redeems → exactly one 200, one user row
TestOnboardInviteEmailConflict email taken meanwhile → 409, link burned
TestOnboardBadBodies unknown fields / missing token → 400

End-to-end once the UI lands (SCRUM-259): admin invites in the Users page → copies the link → the invitee opens it through the tunnel → sets a password → signed in.

Results at time of writing

  • go vet, go test -race -count=2 (7 packages) against Postgres 16: pass.

How it was built

DeepSeek run scoped (Landlock) to services/admin_auth (287 s, ~43k output tokens). Claude review: redemption transaction and conflict paths read; suite run twice. No changes needed.