80 lines
4.7 KiB
Markdown
80 lines
4.7 KiB
Markdown
# 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](../api-contract.md#auth); 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](../../../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](../api-contract.md#the-envelope).
|
|
- **`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](../../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](../config-matrix.md).
|
|
|
|
## Enums
|
|
|
|
| Vocabulary | Values |
|
|
| --- | --- |
|
|
| `PublicRole` | `customer` `nurse` |
|
|
| `AdminRole` | `admin` `support` `finance` `moderation` `super_admin` |
|
|
| `Gender` | `male` `female` — **load-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](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](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 |
|