Перейти к содержанию

Authentication and Session Management

Overview

The auth flow covers the complete lifecycle of a user session in OneWallet — from the initial email OTP verification that starts registration through to JWT-based access on every subsequent request, token refresh, and logout. Auth Center (Serverpod) is the single authority for all authentication. It issues JWTs, manages session state in Redis, and exposes a /auth/validate endpoint that nginx uses as an auth_request gateway to protect Payment Manager and other backend services. Biometric login is available as a convenience path after the first password-based session is established.

Participants

Service Role in This Flow
Flutter App Initiates login/registration; stores tokens in flutter_secure_storage; presents credentials
Auth Center (Serverpod) Issues tokens; validates sessions; manages user lifecycle; exposes /auth/validate
PostgreSQL Stores users, roles, device tokens (public.device_token), user profiles
Redis Stores OTPs (TTL 10 min); session revocation state; account lockout counters
nginx (API Gateway) Performs auth_request to /auth/validate before proxying requests to PM and other services
Payment Manager Receives proxied requests with X-User-Id / X-User-Roles headers injected by nginx

Diagram

Auth Flow

Prerequisites

For new registration: - User has a valid email address not already registered in public.users - SMTP provider is configured in Auth Center

For login (returning user): - User has status other than registration_in_progress or registration_expired - User's account is not locked (fewer than 5 failed login attempts in the last 15 minutes)


Registration Flow (New User — Email OTP)

Step R1: User Enters Email

Service: Flutter App → Auth Center
Action: User types email address. App calls POST /auth/register/email/start.

Rate limits enforced by nginx: max 5 requests/min per IP. Auth Center enforces: max 3 OTP requests per email per 10 minutes (Redis counter otp_rate:<email>).

Duplicate check: if email already exists in public.users with status != 'registration_in_progress' → error "Email already registered".

Step R2: OTP Generated and Sent

Service: Auth Center
Action: Generates a 6-digit OTP. Stores in Redis: SET otp:<email> <code> EX 600 (TTL 10 minutes). Sends via SMTP.
Data: OTP stored in Redis; email sent to user.

Step R3: User Submits OTP

Service: Flutter App → Auth Center
Action: User enters the 6-digit code. App calls POST /auth/register/email/verify with { email, otp }.

Auth Center validates: - OTP matches Redis value for key otp:<email> - Max 5 attempts enforced (Redis counter otp_attempts:<email>) - OTP not expired (Redis TTL check)

Step R4: Temporary User Created and registration_token Issued

Service: Auth Center
Action: On OTP success: 1. Creates a user record:

INSERT INTO users (email, status, registration_expires_at, is_archived, created_at)
VALUES (<email>, 'registration_in_progress', NOW() + INTERVAL '7 days', false, NOW())
2. Issues a registration_token (JWT):
JWT {
  type:       "registration",
  user_id:    <newUserId>,
  expires_in: 7 days,
  algorithm:  ES256
}
3. Deletes OTP from Redis.

Data: registration_token returned to Flutter. Stored in flutter_secure_storage.

Step R5: KYC Flow

Service: Flutter App → Auth Center
Action: All subsequent registration steps (photo upload, KYC processing, data review, T&C acceptance) require registration_token in the Authorization: Bearer header. See kyc-verification.md for full details.

The registration_token is distinct from the access_token — it cannot be used to access payment features.

Step R6: Registration Finalized (KYC Approved)

Service: Auth Center
Action: After operator KYC approval, Auth Center: 1. Updates users.status to the post-KYC state (accessible app with payments pending full verification) 2. Creates a full session: issues access_token (JWT, TTL 15 min) + refresh_token (JWT, TTL 30 days) 3. Stores session in Redis for revocation tracking

Data: Both tokens returned to Flutter. Stored in flutter_secure_storage.


Login Flow (Returning User — Email + Password)

Step L1: User Enters Credentials

Service: Flutter App → Auth Center
Action: User enters email and password. App calls POST /auth/login with { email, password }.

Rate limits: max 10 requests/min per IP (nginx). Account lockout: after 5 failed attempts, Redis counter lockout:<email> blocks login for 15 minutes.

Step L2: Auth Center Validates Credentials

Service: Auth Center
Action: 1. Queries public.users by email 2. Checks is_archived = false and account not locked (Redis lockout:<email>) 3. Verifies password: bcrypt.compare(inputPassword, users.password_hash) (cost factor >= 12) 4. Loads user roles from public.user_roles JOIN public.roles

On success: resets lockout counter.
On failure: increments lockout:<email> counter; if >= 5 → sets TTL 15 min on counter.

Step L3: Session Created — Tokens Issued

Service: Auth Center
Action: Creates JWT pair:

access_token:  ES256, TTL 15 min, payload: { userId, roles[], kycStatus, type: "access" }
refresh_token: ES256, TTL 30 days, payload: { userId, sessionId, type: "refresh" }

Session stored in Redis: SET session:<sessionId> <userId> EX 2592000 (30 days). This enables instant revocation.

Data: Both tokens returned to Flutter. Stored in flutter_secure_storage.

Step L4: Biometric Enrollment (Optional, Post-Login)

Service: Flutter App → Auth Center
Action: After successful login, app offers biometric enrollment. If user accepts, calls POST /auth/biometric/enroll with Authorization: Bearer <access_token>.

Auth Center creates a biometric credential linked to the device. TTL: 30–60 days (configurable via security policy).
Data: Biometric key stored in public.device_token table; device-side key stored in Keychain/Keystore.


Biometric Login Flow

Step B1: Biometric Authentication

Service: Flutter App → Auth Center
Action: User authenticates via device biometrics (fingerprint/Face ID). App retrieves the stored device credential and calls POST /auth/biometric/login.

Auth Center validates: - Biometric credential is active and not expired - Associated sessionId is still valid in Redis

On success: issues new access_token (15 min) and optionally refreshes the biometric credential TTL.


Token Usage on Every Request

Step U1: Flutter Attaches Access Token

Service: Flutter App
Action: Every API call to Serverpod or the PM proxy includes Authorization: Bearer <access_token>.

Step U2: nginx auth_request Validation

Service: nginx → Auth Center
Action: nginx intercepts the request. For protected routes (everything except /auth/*, /webhooks/*, /health), nginx performs a subrequest:

GET /auth/validate
Authorization: Bearer <access_token>

Auth Center validation logic: 1. Verifies JWT signature (ES256) 2. Checks token type = "access" and not expired 3. Checks Redis session:<sessionId> still exists (not revoked) 4. Loads roles from PostgreSQL

Success response (HTTP 200):

{
  "userId": 42,
  "roles": ["user"],
  "kycStatus": "fully_verified",
  "isActive": true
}

nginx injects X-User-Id: 42 and X-User-Roles: user headers and proxies the original request.

Failure (HTTP 401): nginx returns 401 to client; original request is not forwarded.

Step U3: Downstream Service Trusts Headers

Service: Payment Manager (or other backend service)
Action: PM receives the request with X-User-Id already validated by nginx. Does not re-validate the JWT. Uses X-User-Id as the authoritative user identity for all DB operations.


Token Refresh

Step TR1: Access Token Nearing Expiry

Service: Flutter App
Action: Before the access token expires (or after receiving a 401), Flutter calls POST /auth/refresh with { refreshToken }.

Step TR2: Auth Center Rotates Tokens

Service: Auth Center
Action: 1. Validates the refresh_token (signature, expiry, type = "refresh") 2. Checks Redis session:<sessionId> still exists 3. Invalidates old refresh_token (one-time use: deletes old session entry in Redis) 4. Issues new access_token (15 min) and new refresh_token (30 days) 5. Creates new session entry in Redis

Data: New token pair returned to Flutter; old refresh token is invalidated.


Logout

Step LO1: User Logs Out

Service: Flutter App → Auth Center
Action: App calls POST /auth/logout with Authorization: Bearer <access_token>.

Auth Center: 1. Extracts sessionId from the token 2. Deletes session:<sessionId> from Redis — this instantly revokes all further use of both access_token and refresh_token for this session 3. Returns HTTP 200

Data: Session entry deleted from Redis; Flutter clears tokens from flutter_secure_storage.


Device Token Registration

Step DT1: Register for Push Notifications

Service: Flutter App → Auth Center
Action: After successful login (or on app resume), Flutter registers the FCM/APNs device token by calling the device token endpoint.
Data:

INSERT INTO public.device_token (userId, token, platform, createdAt)
VALUES (<userId>, '<fcmToken>', 'android', NOW())
ON CONFLICT (userId, platform) DO UPDATE SET token=<fcmToken>, createdAt=NOW();


JWT Lifecycle Summary

ISSUED      → Returned on login / registration-complete / biometric-login / refresh
   ├─ access_token:  TTL 15 min
   │    Used in Authorization header for every request
   │    Validated by Auth Center via /auth/validate (called by nginx auth_request)
   └─ refresh_token: TTL 30 days
        Stored in flutter_secure_storage
        Used once to obtain new access_token
        Rotated (old invalidated, new issued) on each use
        Revoked instantly via Redis session delete on logout or password change

REVOKED     → DELETE session:<sessionId> from Redis
              Triggers: POST /auth/logout, PUT /auth/password (all sessions), device stolen/compromised

EXPIRED     → access_token after 15 min (client retries with refresh_token)
              refresh_token after 30 days (user must log in again)
              biometric_key after 30–60 days (user must re-enroll biometrics)

Registration Token Lifecycle (Cleanup)

Temporary users who start but do not complete registration are cleaned up by three scheduled cron jobs:

Job Schedule Action
Reminder Daily 09:00 Email reminder to users in registration_in_progress for 5+ days
Archive Daily 02:00 UPDATE users SET is_archived=true, status='registration_expired' where created_at < NOW()-7d
Hard Delete Weekly Mon 03:00 DELETE FROM users WHERE is_archived=true AND archived_at < NOW()-30d; S3 KYC files tagged for deletion

Error Cases

Scenario Where Detected Outcome Recovery
Invalid OTP Auth Center (Step R3) HTTP 401; attempt counter incremented; after 5 attempts: no more checks User requests new OTP after TTL expires
OTP expired (>10 min) Redis TTL / Auth Center HTTP 401 "OTP expired" User requests new OTP
OTP rate limit (3 per 10 min) Auth Center (Step R1) HTTP 429 Wait 10 minutes
Wrong password Auth Center (Step L2) HTTP 401; lockout counter incremented Re-enter correct password
Account locked (5 failed logins) Auth Center (Step L2) HTTP 403 with unlock time Wait 15 minutes
Access token expired nginx auth_request (Step U2) HTTP 401 Flutter uses refresh_token to get new access_token
Refresh token already used (stolen) Auth Center (Step TR2) HTTP 401; session invalidated User must log in again with password
Session revoked (logout on another device) Redis session check (Step U2) HTTP 401 User must log in again
Biometric key expired Auth Center biometric login HTTP 401 User re-enrolls biometrics after password login
Password changed — all sessions invalidated Auth Center PUT /auth/password All Redis session entries for user deleted Users on other devices must log in again
  • /docs/02-auth-module.md — Full auth module documentation (RBAC, token security, cleanup cron jobs, nginx auth_request configuration)
  • /docs/03-kyc-module.md — KYC flow that runs within the registration token context