Skip to main content

Auth backend handoff

Companion to the auth test plan — the case matrix is the single source of truth; this document does not repeat its rows. It gives the backend team everything needed to (a) reproduce any matrix case without any client, (b) mirror the matrix in the backend's own service tests, and (c) answer the contract questions the live runs against backdev surfaced (each tied to a case ID; every claim below marked live-verified was observed on backdev on 2026-07-27 and is recorded in the run protocol).

1. Wire contract the cases assume

Every request carries the app key and the device identity headers — live-verified: without them the backend answers 400 ("Missing api-key field in header" / "Missing device information in header").

HeaderValueNotes
api-keyapp JWT, subject pro_app or portaldrives the flavor gate (314) and device-limit membership
device-typee.g. web, phonedevice-limit membership
device-namefree textshown in the 310 dialog / Devices page
device-idstable UUID per devicesessions are bound to (user, device-id) — see §4
x-user-agentproduct string; the portal sends a value containing the literal enodeportalwebappsee open question Q4
timezoneIANA name
Accept-Languagelocalelocalizes userMessage strings

Auth encodings: OTP request/resend are unauthenticated GETs with the lowercased+trimmed email base64-encoded into the path. Login / register / delete authorize with HTTP Basic base64(email:otp). Everything else sends the session Bearer token.

Curl templates (replayable without any client)

BASE=https://backdev.enode.ai/api
KEY=<pro_app-or-portal api-key JWT>
HDRS=(-H "api-key: $KEY" -H "device-type: web" -H "device-name: curl"
-H "device-id: 00000000-0000-4000-8000-000000000001"
-H "x-user-agent: web/1.0 (curl)" -H "timezone: UTC" -H "Accept-Language: en")
B64() { printf '%s' "$1" | base64; }

# OTP request / resend (login|register)
curl "${HDRS[@]}" "$BASE/users/login/otp/$(B64 user@example.com)"
curl "${HDRS[@]}" "$BASE/users/login/otp/resend/$(B64 user@example.com)"

# Login / register (fixed dev OTP: 123456)
curl "${HDRS[@]}" -X POST -H "Authorization: Basic $(B64 user@example.com:123456)" \
"$BASE/users/login"
curl "${HDRS[@]}" -X POST -H "Authorization: Basic $(B64 user@example.com:123456)" \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","name":"Org","licenseBaseID":"852e8114-0301-4c1e-af30-86f1a77ef118"}' \
"$BASE/users/v3/register"

# Delete flow (Bearer for the OTP request, Basic for the delete; success = 401)
curl "${HDRS[@]}" -H "Authorization: Bearer $TOKEN" \
"$BASE/users/deleteUser/otp/$(B64 user@example.com)"
curl "${HDRS[@]}" -X DELETE -H "Authorization: Basic $(B64 user@example.com:123456)" \
"$BASE/users/delete"

# Legacy migration dispatch (post-auth)
curl "${HDRS[@]}" -H "Authorization: Bearer $TOKEN" \
"$BASE/migrations/enode_pro?fromEmail=$(B64 user@example.com)"

# 310 resolution (token from the 310 body)
curl "${HDRS[@]}" -X DELETE -H "Authorization: Bearer $SESSION_REMOVE_TOKEN" \
"$BASE/user_device_sessions/remove/$SESSION_ID"

# Redirect shapes without state (Bearer required)
curl "${HDRS[@]}" -H "Authorization: Bearer $TOKEN" "$BASE/debug/errors/device-limit" # 310
curl "${HDRS[@]}" -H "Authorization: Bearer $TOKEN" "$BASE/debug/errors/user-not-enabled" # 311
curl "${HDRS[@]}" -H "Authorization: Bearer $TOKEN" "$BASE/debug/errors/open-invitations" # 312
curl "${HDRS[@]}" -H "Authorization: Bearer $TOKEN" "$BASE/debug/errors/abort/314" # 314

2. Backdev conventions the client suite depends on

The live suite's preflight re-proves these every run and fails fast when one breaks — treat them as a stability contract for backdev:

  1. Fixed OTP 123456 for login, register, AND delete (no OTP mails sent).
  2. The /debug/errors/* endpoints stay available (Bearer-authenticated).
  3. Disposable authlive-*@authtest.enode.ai accounts may be created and self-deleted freely; deleted emails stay reserved (the suite never reuses an address).
  4. team@enode.ai and the fixture accounts are never touched destructively (hard-guarded client-side too).

3. Server-side mirror notes (cases the client cannot cover)

CaseWhat only the backend can assert
AUTH-ERR-01Rate-limit thresholds + the 429 body for OTP request/resend (deliberately not probed from the client — it would pollute the shared limiter)
AUTH-ERR-0215-minute OTP expiry; that resend re-mails the SAME session without resetting the 3-attempt/15-min budget
AUTH-REG-09a/d, AUTH-RED-02The real 311 (user-not-enabled) lifecycle: which account state produces it, and the PUT /users/onboarding contract (see Q2)
mail contentAnything about OTP mail rendering/addressing (dev sends none)

4. Open contract questions (each tied to a matrix case)

Q1 — 312 acceptance route (AUTH-RED-03, blocker). Live-verified: a created org child's first authenticated request answers 312 with the full invitations body, but POST /users/{userId}/invitations answers 500 "RouteNotFound.404". The web clients currently cannot resolve a 312 at all. Which route/method/auth accepts invitations? (One orphaned athlete — authlive-ms3p6k85-1ec7-666c-2@authtest.enode.ai, org already deleted — is stuck behind this: even its delete-OTP request answers 312. Please clean it up server-side; it doubles as a repro.)

Q2 — 311 lifecycle (AUTH-RED-02, AUTH-REG-09a/d). Created org children come back enabled (they answer 312, never 311). How does a real user-not-enabled account arise, and is PUT /users/onboarding {name} with a Bearer token the correct resolution (endpoint is unconfirmed in the client)?

Q3 — wrong-OTP statuses (AUTH-LOG-03, AUTH-REG-03, AUTH-DEL-03). Live-verified: a wrong code answers 403, a consumed session 400, and a successful delete 401. Confirm these are intentional and stable — the client docs historically said 401 for a wrong code, and the delete flow's "success = 401" forces the client to prove deletion via a dead-token probe.

Q4 — portal UA gate (AUTH-GATE-01). Live-verified: the portal api-key WITHOUT enodeportalwebapp in x-user-agent logs in fine on backdev. Is the gate prod-only, or removed? (The client keeps sending the marker either way.)

Q5 — social-registered accounts use email OTP (AUTH-LOG-07). Product decision (2026-07-28): social sign-in is retired — Apple/Google-registered accounts MUST authenticate through the normal email OTP flow. Live-verified on backdev: OTP request answers 200 and the OTP login succeeds for such an account. Please confirm this holds on production for every siwa/siwg account (no residual social-only gate anywhere), and treat the historical wrong-flow 400 as removed.

Q6 — OTP device-binding (AUTH-RED-01, AUTH-LOG-04). Live-verified: a login OTP requested by device-id A is rejected for a login with device-id B (400 "Wrong or mismatching email address and passcode"), which contradicts the documented "usable across any number of devices". Which is intended? (Also: docs/auth.md needs updating to the answer.)

Q7 — device limit on backdev (AUTH-RED-01). 8 fresh phone-type device sessions did not trigger a 310 for a fresh Pro org. What is the backdev licence's deviceLimit, so the live test can fabricate a real 310 within a sane attempt cap?

Q8 — dev OTP convention as a contract (§2). Please confirm the fixed-OTP convention (incl. delete) is a deliberate, stable property of develop/backdev so the suite may rely on it long-term.

Q9 — when are the legacy origin reasons emitted? (AUTH-LOG-08/09, AUTH-REG-05/06). Live-verified + product-confirmed 2026-07-28: a legacy-known email (doris@enode.ai, legacy enode_pro, no new-system account) answers plain {reason:"register"} on BOTH OTP requests — this is the expected behavior at the OTP stage (and, on the login request, the seamless-promotion trigger; a truly unknown email answers login instead). Open half: under which circumstances does the backend emit loginPro/loginOne/registerPro/registerOne? The clients dispatch the post-auth legacy migration off exactly these reasons (GET /migrations/{enode_one|enode_pro}?fromEmail=…), so we need the triggering state to exercise that path end-to-end.

Q10 — RESOLVED: One users are blocked on the portal too (AUTH-LOG-11). Product decision (2026-07-28): the observed behavior is correct — a One user logging into the portal answers 314 {missingPrivileges:["canAccessPortal"], currentTier:"Enode One"}, same idea as the Pro-app block. No backend change needed; kept here for the record because it retires the old "One may sign in to the portal to migrate" story (client will retire its allowOneLogin copy accordingly).

Q11 — Pro+ licence base vs the client's Pro check (AUTH-LOG-14, FYI). Live-verified 2026-07-28: the "Enode Pro+" licence has its own base id (7853FCB4-F1C2-4D4F-A822-E51F34A2B9EB), so the client's single-id isProLicenseBaseID stopgap misclassifies Pro+ as One. The client will move to the canAccessPro privilege (present on the Pro+ account) — no backend change needed, listed here so the tier semantics stay aligned.

5. Baseline protocol

The recorded proof run for this handoff is docs/testing/test-runs/2026-07-28-auth-live.md: every matrix ID either executed, skipped-with-reason (missing fixture), or tagged S/E, plus the observation log the live-verified values above come from.