# Backend Phase 2 — Identity: phone-OTP auth, sessions & roles (REST) > **Mission:** give the marketplace its front door. The server already has a working > phone-OTP/JWE/passwordless-TOTP engine and a dynamic-permission RBAC system — but it is reachable > **only over gRPC**, with OTP delivery stubbed to a log line and no session, refresh-rotation, or > role-selection surface. This phase **wraps that existing machinery in REST**: `/auth/otp/request`, > `/auth/otp/verify`, `/auth/refresh`, `/auth/logout`, `/me`, `/me/role`; adds revocable > **`user_sessions`** with refresh-token rotation + stolen-token (reuse) detection; extends `users` with > the load-bearing identity columns (`gender`, `national_id` NULL-until-KYC, `shahkar_verified_at`); and > wires the real **`ISmsSender`** seam so an OTP actually leaves the building (mock = log the code). Do > **not** rebuild auth — reuse `JwtService`, `AppUserManager` OTP, and the RBAC. After this phase every > authenticated feature in the chain has something to authenticate against. > > **Track:** backend · **Depends on:** [b0](./backend-phase-0.md) (REST surface, rate limiter, cross-cutting seams), [b1](./backend-phase-1.md) (config accessor, `notifications`, marketplace migration baseline) · **Unlocks:** every authenticated backend feature; frontend **f1-b2** > **Before you start, read [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md).** It is not optional. --- ## 1. Context — where this sits This is **backend phase b2**, the root of the human-actor identity domain and the gate everything else hangs off — no booking, profile, verification, or payment work can exist without it. The platform has three actor types — **customer** (payer/family), **nurse** (caregiver/seller), **admin** (back-office) — and **phone number is the primary login credential** via phone-OTP; email is optional and only required for admin ([`product/business/01-actors-and-onboarding.md`](../../../product/business/01-actors-and-onboarding.md)). KYC is role-staged: a customer registers and browses with a verified phone alone; a nurse must later pass the full verification pipeline (b6) before becoming bookable. This phase delivers **login + session + role selection** — it deliberately stops short of profiles, patients, and bank accounts (those are b3). **What already exists (do not rebuild) — confirmed in the codebase:** - **The whole auth engine.** [b0](./backend-phase-0.md) kept the working spine: ASP.NET Core Identity, **JWE** access tokens (`Jwt/JwtService : IJwtService` — `GenerateAsync(User)`, `GenerateByPhoneNumberAsync`, `GetPrincipalFromExpiredToken`, `RefreshToken(Guid)`), **phone-OTP** passwordless TOTP (`AppUserManagerImplementation.GenerateOtpCode` / `VerifyUserCode`, `GeneratePhoneNumberConfirmationToken`, `ChangePhoneNumber`; provider constants in `Identity/Dtos/CustomIdentityConstants`), and the **dynamic-permission RBAC** (`DynamicPermissionRequirement`/`Handler`, `DynamicPermissionService.CanAccess`, `ConstantPolicies.DynamicPermission`, `IRoleManagerService`). **Wrap these — never reimplement them.** - **The REST surface & rate limiter.** [b0](./backend-phase-0.md) created the first versioned controllers under `Baya.Web.Api/Controllers/V1/` (sealed, `BaseController`, inject `ISender`, `[controller]`/`[action]` snake_case tokens, `base.OperationResult(...)`), registered `LoggingBehavior`, and wired `AddRateLimiter` + `UseRateLimiter()` with **named per-IP policies** ready for auth/OTP to apply. This phase **applies** those policies — it does not define new middleware. - **Cross-cutting seams.** [b0](./backend-phase-0.md) introduced and DI-registered `IDateTimeProvider`, `IFieldEncryptor` (encrypt/decrypt + deterministic `Hash` for lookups), `ICacheService`, `IObjectStorage`, and `INotificationDispatcher`. **Reuse `IFieldEncryptor`** for the encrypted `phone`/ `email`/`national_id` columns and for the `refresh_token_hash`; **reuse `IDateTimeProvider`** for all expiry/timestamp math. Do not introduce a second clock or encryptor. - **Config & the first migration baseline.** [b1](./backend-phase-1.md) created the marketplace migration baseline, the typed **cached `platform_configs` accessor**, and the `notifications` write path behind `INotificationDispatcher`. Read OTP TTL / session lifetime / max-attempt knobs through the config accessor — never hardcode. Your new tables migrate **onto** b1's baseline. - **`ICurrentUser` + audit interceptor.** [b0](./backend-phase-0.md) wired `ICurrentUser` (Scoped, HTTP- context-backed, null-object for jobs/tests) and the SaveChanges interceptor that stamps `CreatedAt/ModifiedAt/CreatedById/ModifiedById`. Handlers never set audit fields by hand. - **Identity tables & their config.** `User : IdentityUser`, `Role`, `UserRole`, `UserRefreshToken`, the 8 `IEntityTypeConfiguration`s mapping Identity into the **`usr`** schema, and `Baya.Tests.Setup` (in-memory SQLite, full Identity stack incl. passwordless TOTP, real `JwtService`/`AppUserManager`). **You extend these in place** — `users`/`roles`/`user_roles` keep their baseline columns and gain new ones; `user_sessions` is a new table off `users`. **What this phase introduces:** the REST auth controllers, the `users` column extensions, **`user_sessions`** (rotation + reuse detection), the role-selection flow over the existing RBAC, and **one new seam — `ISmsSender`** (the OTP delivery rail; mock = log the code). The customer national-ID KYC path, push/social login, and org self-onboarding are **(DEFERRED)** per the product doc — do not build them. ## 2. Required reading (do this first) - [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md) and [`../_shared/backend-conventions-checklist.md`](../_shared/backend-conventions-checklist.md). - **Product — business truth.** [`product/business/01-actors-and-onboarding.md`](../../../product/business/01-actors-and-onboarding.md) — phone-as-primary-credential, the customer↔patient split, role-staged KYC (national_id NULL until KYC), revocable sessions, MVP-vs-DEFERRED. [`product/overview/platform-summary.md`](../../../product/overview/platform-summary.md) — the four ground truths and where identity sits in the actor model. - **Product — data model.** [`product/data-model/01-identity-and-access.md`](../../../product/data-model/01-identity-and-access.md) — the exact `users` / `user_sessions` / `roles` / `user_roles` columns and constraints you must mirror (`gender` NVARCHAR(10) NULL, `shahkar_verified_at` reset-on-phone-change, `phone` enc UNIQUE, `user_sessions.refresh_token_hash`/`is_revoked`/`revoked_at`/`expires_at`, `user_roles.granted_by`/ `granted_at`/`revoked_at`). - **Engineering truth.** [`server/CLAUDE.md`](../../../server/CLAUDE.md) — *Identity & auth*, *Persistence*, *Startup wiring*, *Project map*; [`server/CONVENTIONS.md`](../../../server/CONVENTIONS.md) — §1 routing, §4 controllers, §6 persistence (audit fields, soft-delete, encrypted PII), §11 security (rate limiting), §12 service registration. - **Contract conventions you must honour.** [`../../contracts/conventions/api-conventions.md`](../../contracts/conventions/api-conventions.md) (envelope, status codes, auth header, pagination) and [`../../contracts/conventions/money-and-types.md`](../../contracts/conventions/money-and-types.md) (the **gender enum is load-bearing** — `male`/`female`, never defaulted/dropped). Section 8 publishes `dev/contracts/domains/identity-auth.md` from the [`_TEMPLATE.md`](../../contracts/domains/_TEMPLATE.md). - **Prior handoff & registry.** `dev/shared-working-context/backend/handoff/after-backend-phase-0.md` and `after-backend-phase-1.md` (what is live), and [`reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) (the `ISmsSender` row you will fill). - **Code to read and mirror** (do not duplicate): `Baya.Application/Features/Users/**` (the existing `UserCreateCommand`, `RefreshUserTokenCommand`, `RequestLogout`, `GenerateUserToken`, `TokenRequest/UserTokenRequestQuery` — these already drive OTP/JWE and are your reuse targets; note the `//TODO Send Code Via Sms Provider` log line you are replacing), `Baya.Application/Features/Admin/**` and `Baya.Application/Features/Role/**` (role/RBAC patterns), `Baya.Infrastructure.Identity/Jwt/JwtService.cs`, `Baya.Infrastructure.Identity/AppUserManagerImplementation.cs`, the existing `UserGrpcServices` (shows the request→`IMediator`→token shape you are re-exposing over REST), `BaseController`, and `WebFramework/Swagger/RequireTokenWithoutAuthorizationAttribute` (mark `/auth/refresh` with it). ## 3. Scope — build this Vertical slices: **entity/migration → command/query handler → controller endpoint → contract**. All amounts/IDs follow project types (`User.Id` is `int`; `UserRefreshToken` is `Guid`-keyed). Reuse the existing OTP/JWE/RBAC plumbing throughout — your handlers orchestrate `IAppUserManager`, `IJwtService`, and the new session repository; they do not re-derive tokens or hash passwords. ### 3.1 Entity & migration changes Add one EF migration (onto b1's baseline) covering: - **Extend `users`** (the `usr` schema Identity table — keep all baseline columns, add via the `UserConfig` configuration + entity properties on `User`): - `gender` NVARCHAR(10) NULL — `male` / `female`. **Load-bearing** for same-gender matching downstream; never defaulted or silently dropped. Not collected at OTP signup; settable later via profile (b3) — here it must merely *exist* and round-trip. - `national_id` (enc) **NULLABLE** — stays NULL until KYC (b6). Do **not** add it to any signup/verify request. `national_id_verified_at` DATETIME2 NULL. - `shahkar_verified_at` DATETIME2 NULL — **reset to NULL on phone change** (see §5). b6 sets it. - `phone` (enc) — must be **UNIQUE** (one identity per phone). `email` (enc) NULLABLE. - `is_active` BIT and `deleted_at` DATETIME2 NULL (soft-delete) with a **global query filter** (`deleted_at IS NULL`) — if the baseline `User` lacks these, add them; otherwise confirm they map. - Encrypted columns route through `IFieldEncryptor`; the UNIQUE on `phone` uses a deterministic `phone_hash` (via `IFieldEncryptor.Hash`) so an encrypted column can still be uniquely indexed — mirror the `iban_hash` pattern documented for b3. - **New `user_sessions`** entity + `IEntityTypeConfiguration` (in a `Persistence/Configuration/UserConfig/` sibling, `usr` schema), off `users`: - `id` (PK), `user_id` (FK → users), `refresh_token_hash` NVARCHAR (store the **hash** via `IFieldEncryptor.Hash`, never the raw token), `device_info` NVARCHAR NULL, `ip_address` NVARCHAR NULL, `is_revoked` BIT, `revoked_at` DATETIME2 NULL, `expires_at` DATETIME2 NOT NULL, `created_at`. - Index `(user_id, is_revoked)` for fast active-session lookup; index on `refresh_token_hash` for rotation lookups. - **Extend `roles` / `user_roles`** (keep baseline RBAC columns): `user_roles` gains the **grant/revoke audit trail** — `granted_by` (FK → users NULL), `granted_at` DATETIME2, `revoked_at` DATETIME2 NULL. Ensure the actor roles `customer` / `nurse` and the admin sub-roles exist (seed `customer`/`nurse` if the b1 seed did not; admin sub-roles `support`/`finance`/`moderation`/`super_admin` are seeded but only ever assigned internally). Add an `IUserSessionRepository` contract in `Application/Contracts/Persistence/` and expose it on `IUnitOfWork` (mirror `IUserRefreshTokenRepository`); implement in Persistence. ### 3.2 Commands / queries (`Baya.Application/Features/Identity/` — new area, or extend `Users/`) Each is a `record` request + `internal sealed` handler; input-bearing commands get a FluentValidation validator; expected failures return `OperationResult` (never throw). Reads are `AsNoTracking()` + `.Select()` projections. - **`RequestOtpCommand(phone)` → `RequestOtpResult`** — normalize/validate the Iranian phone; **upsert** the `users` row (create an inactive-until-verified user if new, via the existing `UserCreateCommand` path / `IAppUserManager`); call `GenerateOtpCode`; **deliver via `ISmsSender`** (replacing the `//TODO Send Code Via Sms Provider` log). Return a non-enumerating result (e.g. `otp_sent: true`, `resend_available_in_seconds` from config) — **do not leak whether the phone already existed**. - **`VerifyOtpCommand(phone, code, device_info?)` → `AuthTokensResult`** — `VerifyUserCode`; on success mark `phone_verified_at`/`is_active`, mint a **JWE access token** (`IJwtService.GenerateByPhoneNumberAsync`) **and a refresh token**, and **create a `user_sessions` row** storing `refresh_token_hash`, `expires_at` (from config), `device_info`, `ip_address` (from `ICurrentUser`/HTTP context). Return `{ access_token, refresh_token, access_expires_at, refresh_expires_at, is_new_user, roles[] }`. On a fresh user `roles` is empty (drives the f1 role-router to `/me/role`). - **`RefreshTokenCommand(refresh_token)` → `AuthTokensResult`** — look up the session by `Hash(refresh_token)`. **Rotation + reuse detection:** if the session is found and active → mark it revoked, mint a new access+refresh pair, create a **new** session (rotation). If the presented token hashes to a session that is **already revoked** → treat as **stolen-token reuse**: revoke **all** of that user's active sessions (logout-everywhere) and return `401`. Expired session → `401`. Mark the controller action with `RequireTokenWithoutAuthorizationAttribute` (it runs without a valid access token). - **`LogoutCommand(refresh_token? | current session)` → `OperationResult`** — revoke the current session (`is_revoked=true`, `revoked_at=now`); rotate the security stamp via the existing `RequestLogout` path so the JWE `OnTokenValidated` security-stamp check rejects the old access token too. Optional `everywhere: true` revokes all the user's sessions. - **`GetMeQuery()` → `MeResult`** — current user (id, phone-masked, first/last name, gender, `is_active`), **roles[]**, **profile-completion** flags (e.g. `has_customer_profile`, `has_nurse_profile` — false for now; b3 populates the underlying tables), and **nurse verification status** (read from `nurse_verifications.status` when present; until b6 it returns `not_started`/`null` — never read a removed `nurse_profiles.verification_status`). Projection only; `AsNoTracking()`. - **`SelectRoleCommand(role)` → `MeResult`** — assign **`customer` or `nurse`** to the current user via `user_roles` (audit `granted_by` = self / system, `granted_at`). **Reject any admin sub-role** with `403` — admin roles are internal-only and never self-assignable (§5). Idempotent if the role is already held. A user may hold both `customer` and `nurse`. ### 3.3 Controllers (`Baya.Web.Api/Controllers/V1/`) Sealed, inherit `BaseController`, inject `ISender`, return `base.OperationResult(...)`, snake_case `[controller]`/`[action]` routes, `CancellationToken` threaded. Apply the b0 rate-limit policies as noted. | Method & route (snake_case) | Request → handler | Auth | Rate-limited | | --- | --- | --- | --- | | `POST api/v1/auth/otp/request` | `RequestOtpCommand` | none | **yes** (per-phone + per-IP OTP policy) | | `POST api/v1/auth/otp/verify` | `VerifyOtpCommand` | none | **yes** (verify/brute-force policy) | | `POST api/v1/auth/refresh` | `RefreshTokenCommand` | `RequireTokenWithoutAuthorization` | **yes** (refresh policy) | | `POST api/v1/auth/logout` | `LogoutCommand` | authenticated | no | | `GET api/v1/me` | `GetMeQuery` | authenticated | no | | `POST api/v1/me/role` | `SelectRoleCommand` | authenticated | no | > Routes render through the snake_case transformer; document the exact emitted paths in the contract from > the published `swagger.json` (don't assume the token expansion). ### 3.4 Wire `ISmsSender` for OTP delivery Introduce **`ISmsSender`** (Application contract) with a real-shaped signature (`Task SendOtpAsync(string phone, string code, CancellationToken ct)` plus a generic `SendAsync(string phone, string message, ...)` for later transactional SMS). The **mock** Infrastructure implementation **logs the code** (structured, never the full PII number in plaintext beyond what logging policy allows) and returns success; selection is by config/registration (`ServiceConfiguration/` extension), never an `if (mock)` in the handler. `RequestOtpCommand` calls it after `GenerateOtpCode`. ### 3.5 Out of scope (DEFERRED — do not build) - Customer national-ID KYC (`customer_profiles.national_id_verified_at`) — column deferred; never gate browsing on it. → deferred per [`product/business/01-actors-and-onboarding.md`](../../../product/business/01-actors-and-onboarding.md) §(c). - Push-notification registration, social login, nursing-company (org) self-onboarding. → same doc. - Profiles, patients, addresses, nurse bank accounts → **[b3](./backend-phase-3.md)**. - The `is_verified` flip, Shahkar re-verification on phone change (the *handler* that re-runs Shahkar) → **[b6](./backend-phase-6.md)**. This phase only **resets `shahkar_verified_at` to NULL** when the phone changes; it does not run Shahkar. ## 4. Mocks & seams in this phase | Seam | Owner | Mock behaviour | Registry | | --- | --- | --- | --- | | **`ISmsSender`** | **introduced here** | logs the OTP code (structured); returns success. Selected by config; real Kavenegar/Ghasedak client swaps in later (implement the same interface, keep idempotency/rate-limit logic). | **add row → 🟡** | | `IFieldEncryptor` | reuse (b0) | encrypt/decrypt `phone`/`email`/`national_id`, hash `phone`/`refresh_token`. | no change | | `IDateTimeProvider` | reuse (b0) | clock for OTP/session expiry. | no change | | `ICacheService` | reuse (b0) | config-accessor cache (b1) only; not new here. | no change | | `INotificationDispatcher` | reuse (b0/b1) | optional "new login" notification (in-app, b1 write). | no change | Record `ISmsSender` in [`reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md): seam name + file, what's faked, config keys (gateway api-key, sender line, OTP TTL), step-by-step how to make it real (pick gateway, add client package to `Directory.Packages.props`, implement `SendOtpAsync`, register over the mock by config), status 🟡. ## 5. Critical rules you must not get wrong - **Phone is the primary credential.** OTP is the only public login. **Email is optional** and required only for admin — never make email mandatory, never make it a login key. - **`national_id` is NOT collected at signup** and stays **NULL until KYC** (b6). An unverified registration must never look KYC-complete. Do not add `national_id` to any auth request body. - **`shahkar_verified_at` resets to NULL on phone change.** Whenever the stored phone changes (via `ChangePhoneNumber`), null out `shahkar_verified_at` so b6 re-verifies. Don't silently keep a stale binding. - **Sessions are revocable with refresh-token rotation + reuse detection.** Each refresh **rotates** (old session revoked, new one issued). A refresh presented against an **already-revoked** session is a **stolen-token signal** → revoke all the user's sessions and `401`. Store only the **hash** of the refresh token (`IFieldEncryptor.Hash`), never the raw token. Logout must invalidate the session server-side (and rotate the security stamp so the access token dies too). - **Admin roles are NEVER self-assignable** via the public `/me/role`. `SelectRoleCommand` accepts only `customer` / `nurse`; any admin sub-role → `403`. Admin provisioning is internal-only and goes through the RBAC grant path (audited `granted_by`). - **OTP is rate-limited and codes expire.** Apply the b0 rate-limit policies to `otp/request`, `otp/verify`, and `refresh`. Read TTL/max-attempt/lockout knobs from the b1 config accessor. Treat brute-force / lockout as an explicit handled `400`/`429` state — never an unhandled exception. Do not enumerate accounts: `otp/request` returns the same shape whether or not the phone existed. - **`users.gender` is load-bearing** (`male`/`female`) for same-gender matching — the column must exist and round-trip cleanly now even though it is populated later; never default it to a value or drop it. - **Encrypt PII at rest** (`phone`, `email`, `national_id`) via `IFieldEncryptor`; never log plaintext PII or project it into non-authorized responses (`/me` masks the phone). - **Reuse, don't rebuild.** Tokens come from `IJwtService`; OTP from `IAppUserManager`; RBAC from the existing permission system. No parallel JWT issuer, no second OTP generator, no `if (mock)` branches. ## 6. Definition of Done The shared [definition-of-done.md](../_shared/definition-of-done.md), plus: - [ ] The six endpoints exist, are reachable through Swagger, and return the `OperationResult` envelope; `otp/request`/`otp/verify`/`refresh` are rate-limited; `refresh` carries `RequireTokenWithoutAuthorization`. - [ ] `users` is extended (`gender`, `national_id` enc NULL, `national_id_verified_at`, `shahkar_verified_at`, `phone` enc UNIQUE via hash, `email` enc NULL, `is_active`, `deleted_at` + soft-delete query filter); `user_sessions` and the `user_roles` audit columns exist; one clean migration onto b1's baseline (`dotnet build` zero new warnings, `dotnet ef migrations` applies). - [ ] Refresh rotation works and a **replayed/old refresh token is rejected** with all sessions revoked; logout invalidates the session **and** the access token (security-stamp rotation). - [ ] `SelectRoleCommand` assigns `customer`/`nurse` only; an admin sub-role attempt returns `403`. - [ ] `ISmsSender` is introduced behind DI, the mock logs the code, the registry row is added (🟡), and `RequestOtpCommand` calls it (the old TODO log is gone). - [ ] Handler unit tests (NSubstitute) for verify/refresh/role + at least one `WebApplicationFactory` integration test per endpoint area (happy path, 401 on `/me` unauthenticated, 400 on bad OTP, reuse-detection 401 on refresh, 403 on admin self-assign). `dotnet test Baya.sln` green. - [ ] The **Project map** in `server/CLAUDE.md` notes the new `Identity` feature area, `user_sessions`, the auth controllers, and the `ISmsSender` seam. ## 7. How to test (what a human can verify after this phase) A reachable SQL Server is required. Run the API (`dotnet run --project src/API/Baya.Web.Api/...`), open `/swagger`. 1. **OTP request logs a code.** `POST api/v1/auth/otp/request` with `{ "phone": "09120000000" }` → `200` `{ otp_sent: true, ... }`; the **OTP code appears in the server log** (the `ISmsSender` mock). Repeat immediately past the limit → `429`. 2. **Verify mints tokens + creates a session.** `POST api/v1/auth/otp/verify` with the phone + the logged code → `200` with `access_token`, `refresh_token`, expiries, `is_new_user: true`, `roles: []`. Confirm a `user_sessions` row exists (`is_revoked=false`). 3. **`/me` returns the user + roles.** `GET api/v1/me` with `Authorization: Bearer ` → `200` with masked phone, empty `roles`, profile-completion flags false, nurse verification `not_started`/null. Without the header → `401`. 4. **Role selection.** `POST api/v1/me/role` `{ "role": "customer" }` → `200`, `/me` now shows `["customer"]`. `{ "role": "nurse" }` also succeeds (a user can be both). `{ "role": "super_admin" }` → `403`. 5. **Refresh rotates; replay is rejected.** `POST api/v1/auth/refresh` with the refresh token → `200` new pair (old session now revoked). Replay the **same old** refresh token → `401`, and confirm **all** that user's sessions are revoked (stolen-token detection). 6. **Logout kills the session and the access token.** `POST api/v1/auth/logout` → `200`; the prior `access_token` now fails `/me` with `401` (security-stamp rotation), and the session is `is_revoked`. 7. **Bad OTP.** `otp/verify` with a wrong/expired code → `400` with a safe message (no enumeration). These steps become the "what is now testable" section of your report. ## 8. Hand off & document (close the phase) - **Docs to update (same change):** `server/CLAUDE.md` — *Project map* (new `Features/Identity` area, `user_sessions` table + config, the V1 auth controllers, the `ISmsSender` seam and where it's registered) and *Identity & auth* (note OTP delivery is now real-seamed, sessions exist, role-selection flow). If you decided any rule not already in the product docs (e.g. the exact reuse-detection response), reflect it in [`product/business/01-actors-and-onboarding.md`](../../../product/business/01-actors-and-onboarding.md) — record decisions, don't invent rules. - **Contract to write:** publish **`dev/contracts/domains/identity-auth.md`** from the [`_TEMPLATE.md`](../../contracts/domains/_TEMPLATE.md) — the six endpoints (routes, request/response shapes, auth, rate-limited flags, status codes incl. `401` reuse-detection and `403` admin self-assign), the `role` enum (`customer`/`nurse` public; admin sub-roles internal), the `AuthTokensResult` / `MeResult` / `RequestOtpResult` shared shapes (mark the masked phone), and the gender enum note. Then **publish the `swagger.json` snapshot** per [`openapi/README.md`](../../contracts/openapi/README.md) so f1-b2 derives types from it, not by guessing. - **Handoff & report:** write `dev/shared-working-context/backend/handoff/after-backend-phase-2.md` (auth is live over REST; what f1-b2 can now build — login, OTP, role router, `AuthContext` roles; what's mocked = SMS delivery), append to `backend/STATUS.md`, write `dev/shared-working-context/reports/backend-phase-2-report.md` (what was built, what is testable and exactly how — the §7 steps, what is mocked + how to make `ISmsSender` real, the `identity-auth` contract produced, follow-ups: profiles/patients/bank in b3, Shahkar/`is_verified` in b6), and update [`reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) (the `ISmsSender` row → 🟡). - **Memory:** save a `project` memory note for the non-obvious decisions — how REST wraps the existing gRPC/OTP/JWE engine without duplication, the refresh-rotation + reuse-detection design (revoke-all on replay), the `phone_hash` UNIQUE-on-encrypted pattern, and the rule that admin roles are never self-assignable — with a one-line `MEMORY.md` pointer.