Files
sxchnge/docs/auth-notifications.md

9.5 KiB

Signup and login email integration

Task #38, PR #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.

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:

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 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.