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), audituser.onboard. If the email became an account meanwhile → 409already_existsand 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_failednaming 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.