147 lines
8.8 KiB
Markdown
147 lines
8.8 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.
|
|
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.
|
|
|
|
## Configuration
|
|
|
|
Load the `auth.activation` and `auth.login` revision 1 templates from
|
|
`backend/notifications/templates.example.json` into the notification catalog.
|
|
Both 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.
|
|
- `token_env`: environment variable containing the configured caller's token
|
|
(at least 32 bytes); example `SXCHNGE_NOTIFICATIONS_TOKEN`.
|
|
- `ca_file`: trusted PEM CA bundle; omit to use system roots.
|
|
- `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 caller token must map to the product caller in the notification service's
|
|
configuration. Keep Mongo, notification caller, 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 and therefore requires credentials.
|
|
|
|
The notification service must be reachable over TLS, 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 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 |
|
|
|
|
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 destination per hour. 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.
|
|
|
|
Code consumption precedes session creation. If session creation or its HTTP
|
|
response fails, restart password login; the code is not reusable. 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.
|