Files
baya-monorepo/docs/integration/domains/auth.md
T
2026-07-30 12:49:46 +03:30

4.7 KiB

auth — phone OTP, sessions, /me, role selection

Client seam client/src/services/auth/ · USE_AUTH_MOCK = false (real) · 7 server ops Last verified: 2026-07-30 against commit d3ec723 and swagger.v1.json (2026-07-29).

The only way into the platform. Phone + OTP, no passwords. The transport rules — bearer header, cookie storage, silent refresh, rotation, reuse detection — are in ../api-contract.md; this file is the endpoints and the payload semantics.

Endpoints

Method Path Auth Rate limit Verdict
POST /api/v1/auth/request_otp anonymous otp 5/min wired
POST /api/v1/auth/verify_otp anonymous otp 5/min wired
POST /api/v1/auth/refresh anonymous auth 10/min wired · called by both services/auth and the fetch layer
POST /api/v1/auth/logout [Authorize] wired
GET /api/v1/me [Authorize] wired
POST /api/v1/me/select_role [Authorize] wired
GET /api/v1/dev/last_otp/{phone} anonymous Development only — see below

No phantoms.

AUTH_API_BASE is /api/v1; the routes above are exactly what the client sends.

dev/last_otp — live on the deployment

DevController is registered unconditionally, and the OTP-capture bridge behind it is wired only when IsDevelopment() and the SMS provider is capture-safe (mock or telegram). The balinyaar.ir deployment runs ASPNETCORE_ENVIRONMENT=Development with Seams:Sms:Provider = telegram, so both conditions hold and the endpoint is reachable on api.balinyaar.ir. Anyone who knows a registered phone number can read its login code. Recorded in DEPLOY.md as the deployment's largest exposure; the fix is the environment switch, not a code change.

Selecting a real gateway (kavenegar, …) disables the bridge — the OTP must never be logged or captured once real SMS ships.

Shape rules the JSON does not express

  • RequestOtpResult carries the code length and expiry (REQ-002, delivered) so the client sizes the input and runs the countdown from server truth rather than a hardcoded constant.
  • verify_otp failures carry a machine-readable code on the envelope (REQ-003, delivered) — e.g. otp_locked — so the client branches on the state instead of matching a message string. This is the code field described in ../api-contract.md.
  • refresh returns a new pair and retires the old refresh token. Presenting a retired token is treated as theft: the session is killed, not merely refused. A 401 from /auth/refresh is therefore terminal and the client must not retry it.
  • /me is the only source of identity. The JWE is opaque; the client reads role, gender and profile-completeness from /me, never from a decoded claim.
  • Multi-role users: /me reports the roles the caller holds. POST /me/select_role commits to one. REQ-004 was resolved as a client concern — the client owns the disambiguation and the "resolved vs. pending" role hydration; no backend change was needed. See docs/rules/client/auth.md.
  • logout returns an empty envelope — no data. The client awaits the revocation and must not unwrap() it.
  • Phone numbers are encrypted at rest. Login looks the user up by a deterministic HMAC hash (users.PhoneHash, derived from Seams:FieldEncryption:HashKey), never by comparing the encrypted column. This is why that key is immutable — see ../config-matrix.md.

Enums

Vocabulary Values
PublicRole customer nurse
AdminRole admin support finance moderation super_admin
Gender male femaleload-bearing, drives same-gender caregiver matching
NurseVerificationStatus (as surfaced on /me) not_started in_progress pending_review verified rejected

The /me verification summary uses a different vocabulary from the verification domain's own aggregate status (not_started pending in_review approved rejected suspended). They are two read models over the same source of truth, not a drift — but do not treat the strings as interchangeable. See verification.md.

Open REQs

REQ Status Effect
REQ-038 open /me carries no signal that the caller administers a partner center, so partner auto-routing cannot be driven off it. See partner-center.md
REQ-039 open The OTP SMS template is not WebOTP-conformant, so the browser's one-tap autofill never fires. The client's WebOTP hook ships anyway and degrades silently