Files
sxchnge/docs/auth-notifications.md
T

155 lines
9.5 KiB
Markdown

# Signup and login email integration
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.
The initial integration is merged. Task #40 separates login verification into
the [second-factor service](architecture/two-factor.md).
## Configuration
Load the `auth.activation`, `auth.login` and `twofactor.code` revision 1 templates from
`backend/notifications/templates.example.json` into the notification catalog.
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.
Configure `api.notifications` in the product YAML:
- `address`: internal notifications gRPC address.
- `ca_file`: optional trusted PEM CA bundle for explicit TLS. Omit both TLS settings for plaintext.
- `server_name`: optional TLS certificate hostname override, never a TLS bypass.
- `activation_url`: absolute frontend `/verify-email` URL. HTTPS is required,
except HTTP on localhost/127.0.0.1 for development. No user-supplied redirect is
accepted. The token is appended as a query parameter.
The private notifications endpoint uses the existing product storage namespace
without caller authentication. Keep Mongo, TLS and provider credentials out
of Git. JWT signing configuration must contain at least 32 bytes of private key
material; HMAC-derived issuance keys depend on this key. Rotation changes replay
keys and existing pending JWT validity. Omission of `api.notifications` preserves
the bootstrap-only legacy routes; it does not enable signup or code completion.
The example config enables the integration; configure Mongo, NATS and email credentials separately.
The notification service must be reachable on the private network, with its Mongo/NATS/email
provider configured. Product startup creates a lazy gRPC channel; dependency
outages appear as HTTP 503 when attempting delivery. A successful Send means
persisted queue acceptance, not inbox delivery or a provider receipt.
Start Flutter with:
```sh
flutter run --dart-define=SXCHNGE_API_URL=https://api.example/api/v1 \
--dart-define=SXCHNGE_CLIENT_ID=your-provisioned-public-client
```
Provision that client using the existing local application-client configuration
workflow. Its ID must exist in the BFF's client store and allow the user's IP.
Do not put a confidential client secret in Flutter. Match BFF CORS to the exact
frontend origin. Browser requests use credentials, the existing CSRF headers,
and Secure/HttpOnly/SameSite=Strict refresh cookies; deploy frontend and API on
HTTPS origins within the same site. Native adapters retain credentials in memory.
No API URL keeps the existing explicit preview services. With an API URL, auth
is real, the preview shortcut is hidden, and workspace routes require the local
confirmed session. Wallet, identity and factor-settings backend adapters remain
outside this change; those screens still use their existing preview data. The
real factor adapter refuses unsupported money/settings operations.
## HTTP contract
All paths have prefix `/api/v1/accounts`. Responses use the existing `data`
envelope. Credentials, codes and activation tokens are sent in POST bodies.
| Route | Request | Successful result |
| --- | --- | --- |
| POST `/signup` | `name`, `email`, `password`, `locale`, `idempotencyKey` | 202 with `userId`, after configured Facts intake and 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}`, `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.
The API-login client-secret endpoint retains its pre-existing separate behavior.
Unverified customers cannot log in; Flutter routes them to verification recovery.
Passwords preserve whitespace, matching signup hashing. Activation is single-use
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 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.
New idempotency keys are server-secret-derived before token generation. Automatic
verification keys now use 32 cryptographically random bytes. Empty-target queries
match both omitted and empty-string stored fields, fixing Mongo cooldown,
invalidation and replay semantics without rewriting existing documents.
Consumed or locked records cannot be replayed to redeliver an unusable code.
Token/hash/salt/key previews are excluded from verification logs.
## Recovery and delivery limits
A resend retries its same idempotency key after a failed/ambiguous RPC. Token
replay reconstructs the same code/link and stored expiry, and notification Send
deduplicates the same payload. A new accepted resend gets a new key. A failed
initial signup leaves the account pending; retry the same credentials or request
another activation email after cooldown. Initial signup and password-login
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.
When `api.sendico` is enabled, sign-up and activation resend first require a
durable Facts intake receipt. Activation checks that receipt in its transaction.
Initial account/token creation uses the same server-secret-derived activation
key as a same-key retry, so an intake outage does not force an extra cooldown for
an unsent initial token. Existing pending customers created before this change
retain the prior cooldown behavior. See [Facts integration](signup-facts.md) for
uncertain registration recovery and the `signup_attention_required` error.
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
in the notification service; no inbox/delivery-receipt status is claimed here.
No live emails were sent during implementation tests.
## Measurement and UX review
Existing `InstrumentedAuthServices`, `JourneyRecorder`, and factor providers track
signup, initial email acceptance, confirmation, login challenge verification,
completion, failure, retry, and navigation abandonment. Requests retain the
existing journey correlation and timeout rules in `docs/ux/auth-journeys.md`.
The initial confirmation-page send consumes the signup queue receipt once to
avoid sending a duplicate email. Success is based on backend acceptance or
verification, with `simulated: false`; it never means an inbox receipt.
Backend metrics `sxchnge_auth_flow_requests_total{operation,outcome}` and
`sxchnge_auth_flow_duration_seconds{operation}` expose accepted, rejected, limited
and unavailable HTTP steps without email, tokens or account labels. Combine
login-verify acceptance with login requests to inspect completion and dependency
failures. Counts are HTTP attempts, so retries are not distinct users. The current
frontend analytics sink is still the existing injected/default preview sink;
a production sink and cross-device confirmation correlation remain deployment
work. Do not interpret default in-memory analytics as retained production data.
UX checks cover unchanged mobile/keyboard form composition, preserved input after
errors, loading and cooldown feedback, invalid/expired/locked code recovery,
unverified-account recovery, and English/Russian copy. Queue feedback is phrased
conditionally because absent/already-active emails deliberately receive the same
acceptance response. Auth credentials are absent from provider state, analytics,
URLs (except the existing activation link) and local persistent storage. Existing
widget suites cover screen behavior; new adapter tests cover real response state
mapping, duplicate initial delivery, failures and one-use completion grants.
## Validation
Required validation: `make test` (including `make test-frontend`),
`make lint-backend`, Dart format checks for every changed Dart file,
and `flutter analyze` in `frontend/packages/sxchnge_auth` and `frontend/app`.
Results and the delivery PR are recorded in `docs/implementation.md` and task #38.