8.8 KiB
Signup and login email integration
Task #38, PR #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); exampleSXCHNGE_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-emailURL. 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:
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.