Separate second-factor verification and delivery from auth (#40)

This commit is contained in:
Stephan D committed 2026-09-12 20:36:35 +02:00
1 parent 12a38cd654
commit ecb63b61c6
33 files changed
+1772 -123

No files matched your search

+12 -10
View File
@@ -2,14 +2,14 @@
Task [#38](https://git.sendico.io/tech/sxchnge/issues/38), [PR #39](https://git.sendico.io/tech/sxchnge/pulls/39), branch `auth-38`.
Primary module: auth; secondary modules: notification templates and Flutter auth.
This change stacks on notifications PR #37, which stacks on BFF PR #35.
Merge #35, then #37, then this contribution, retargeting each dependent PR to main.
The initial integration is merged. Task #40 separates login verification into
the [second-factor service](architecture/two-factor.md).
## Configuration
Load the `auth.activation` and `auth.login` revision 1 templates from
Load the `auth.activation`, `auth.login` and `twofactor.code` revision 1 templates from
`backend/notifications/templates.example.json` into the notification catalog.
Both templates have English and Russian variants; product account locale selects
These templates have English and Russian variants; product account locale selects
Russian for `ru` variants and English otherwise. Publish subsequent template
changes under a new revision rather than mutating deployed content.
@@ -66,9 +66,9 @@ envelope. Credentials, codes and activation tokens are sent in POST bodies.
| POST `/signup` | `name`, `email`, `password`, `locale`, `idempotencyKey` | 202 with `userId`, after activation email queue acceptance |
| POST `/verification/send` | `email`, `idempotencyKey` | 202; generic acceptance also applies to absent/already-active accounts |
| POST `/verification/confirm` | `token` | 202 with `verified: true` after activation commits |
| POST `/login` | `login: {login, password}`, `clientId`, `deviceId` | 202 with `userId`, `pendingToken: {token, expiration}`, masked `target`, `expiresAt`, `resendAt` |
| POST `/login/resend` | `pendingToken`, `idempotencyKey` | 202 with the challenge timing and same pending credential |
| POST `/login/verify` | `pendingToken`, six-digit `code` | 200 with account and existing access/refresh response |
| POST `/login` | `login: {login, password}`, `clientId`, `deviceId` | 202 with `userId`, `pendingToken: {token, expiration}`, `challengeId`, `channel`, masked `target`, `expiresAt`, `resendAt` |
| POST `/login/resend` | `pendingToken`, `challengeId`, `idempotencyKey` | 202 with the challenge timing and same pending credential |
| POST `/login/verify` | `pendingToken`, `challengeId`, six-digit `code` | 200 with account and existing access/refresh response |
The pending JWT binds the customer, client and device. The verification record
binds its code to a hash of that JWT. Password login does not create a session.
@@ -79,7 +79,8 @@ and commits the account transition with token consumption in one transaction.
Login codes expire within 10 minutes and cannot outlive their pending JWT.
Resending uses a one-minute cooldown, invalidates the previous code in that
challenge, and allows five deliveries per destination per hour. Incorrect codes
challenge, and allows five deliveries per customer and twenty per requesting IP per hour
across second-factor purposes. Incorrect codes
consume the existing five-attempt budget. Restart login after a pending credential
expires or a challenge locks. Signup/resend request limits also apply per email
(10/hour) and IP (30/hour), using shared atomic fixed-window counters.
@@ -102,8 +103,9 @@ retries can create replacement messages; they are not distributed exactly-once
operations. Tokens are stored before the RPC, so a process crash between those
steps requires user recovery; there is no product-side delivery outbox.
Code consumption precedes session creation. If session creation or its HTTP
response fails, restart password login; the code is not reusable. The adapter
Code verification, one-use proof consumption and session creation now commit in
one transaction. A session-write failure leaves the code retryable; a lost HTTP
response after commit requires restarting password login. The adapter
retains a confirmed session in memory and has no refresh/restore-on-reload flow
in this contribution. Backend authorization remains authoritative for future
protected APIs. Notification provider failures after queue acceptance are visible