Authentication & Session Management¶
4pass uses a layered authentication architecture: JWT tokens for session identity, Argon2id for password verification, per-session CSRF tokens, Cloudflare Turnstile for bot prevention, and an ordered middleware stack that enforces security policies on every request.
JWT Token Architecture¶
sequenceDiagram
participant Browser
participant API as FastAPI
participant CF as Cloudflare Turnstile
participant DB as PostgreSQL
participant Redis as Valkey
Browser->>API: POST /auth/login<br/>{email, password, turnstile_token}
API->>CF: Verify Turnstile token
CF-->>API: ✓ Human verified
API->>DB: SELECT user WHERE email = ?
DB-->>API: User record
API->>API: Argon2id verify(password, hash)
API->>DB: INSERT user_session<br/>(csrf_token, ip, user_agent)
DB-->>API: Session created
API->>API: Sign JWT access token (30 min)<br/>Sign JWT refresh token (30 days)
API-->>Browser: Set-Cookie: access_token (HttpOnly, Secure, SameSite=strict)<br/>Set-Cookie: refresh_token (HttpOnly, Secure, SameSite=strict)<br/>Body: {user, csrf_token}
Note over Browser: CSRF token stored<br/>in sessionStorage
Browser->>API: GET /trading/accounts<br/>Cookie: access_token<br/>X-CSRF-Token: {token}
API->>API: Verify JWT signature + expiry
API->>DB: Load user + session
API->>API: Validate CSRF (hmac.compare_digest)
API-->>Browser: 200 OK + data
JWT tokens are never exposed to JavaScript. Both access and refresh tokens live in HttpOnly cookies — the frontend doesn't store, read, or transmit tokens manually. This eliminates XSS-based token theft entirely.
Token Management¶
| Property | Access Token | Refresh Token |
|---|---|---|
| Lifetime | 30 minutes (ACCESS_TOKEN_EXPIRE_MINUTES) |
30 days (REFRESH_TOKEN_EXPIRE_DAYS) |
| Storage | HttpOnly cookie (access_token) |
HttpOnly cookie (refresh_token) |
| Algorithm | HS256 | HS256 |
| SameSite | strict |
strict |
| Secure | true (HTTPS only) |
true (HTTPS only) |
| Domain | Current domain (COOKIE_DOMAIN) |
Current domain |
Token sources are checked in order:
- Cookie (primary) —
access_tokencookie, used by browser clients - Bearer header —
Authorization: Bearer <token>, used by API clients and Swagger UI
Token refresh flow:
sequenceDiagram
participant Browser
participant API as FastAPI
participant DB as PostgreSQL
Browser->>API: POST /auth/silent-refresh<br/>Cookie: refresh_token
API->>API: Verify refresh token JWT
API->>DB: Validate session still active
DB-->>API: Session valid
API->>API: Issue new access token (30 min)
API->>DB: Optionally rotate refresh token
API-->>Browser: Set-Cookie: access_token (new)<br/>Set-Cookie: refresh_token (rotated)
Silent Refresh
The /auth/silent-refresh endpoint is excluded from CSRF validation because it relies on the SameSite=strict refresh token cookie for authentication — a CSRF attacker cannot cause the browser to send this cookie cross-origin.
Password Security¶
4pass uses Argon2id via pwdlib[argon2] — the OWASP-recommended password hashing algorithm for high-value accounts.
| Property | Value |
|---|---|
| Algorithm | Argon2id (memory-hard + GPU-resistant) |
| Library | pwdlib with PasswordHash.recommended() defaults |
| Why not bcrypt | bcrypt's 72-byte input limit and lower memory cost make it weaker against GPU/ASIC attacks |
| Why not scrypt | Argon2id is the PHC (Password Hashing Competition) winner and has stronger side-channel resistance |
from pwdlib import PasswordHash
password_hash = PasswordHash.recommended()
hashed = password_hash.hash("user_password")
verified = password_hash.verify("user_password", hashed)
Session Management¶
Each login creates a new session record in the user_sessions table:
| Field | Type | Purpose |
|---|---|---|
id |
UUID | Unique session identifier |
user_id |
FK → users | Session owner |
csrf_token |
String (32-byte) | Per-session CSRF token for synchronizer pattern |
ip_address |
String | Client IP at login (via trusted proxy chain) |
user_agent |
String | Browser User-Agent at login |
created_at |
Timestamp | Session creation time |
is_active |
Boolean | false on logout or revocation |
Session lifecycle:
- Creation: On successful login, after Argon2id verification and CAPTCHA check
- Validation: On every authenticated request — JWT decoded, session loaded from DB,
is_activechecked - Revocation: On explicit logout (POST /auth/logout) or administrative action
- Expiration: Subscription expiration checks on each request — expired users lose access to premium features
CSRF Protection¶
4pass implements the Synchronizer Token Pattern with per-session tokens and constant-time comparison.
| Property | Detail |
|---|---|
| Pattern | Synchronizer Token (per-session, server-validated) |
| Header | X-CSRF-Token |
| Protected methods | POST, PUT, DELETE, PATCH |
| Token generation | 32-byte secrets.token_urlsafe() created per session |
| Comparison | hmac.compare_digest() — constant-time to prevent timing attacks |
Excluded paths (no CSRF required):
| Path | Reason |
|---|---|
/webhook/* |
TradingView webhooks use their own 4-layer authentication |
/auth/login, /auth/register |
No session exists yet |
/auth/token |
OAuth2 standard token endpoint |
/auth/forgot-password, /auth/reset-password |
Pre-authentication flows |
/auth/silent-refresh |
Protected by SameSite=strict cookie |
/auth/public-key |
Public key retrieval (read-only) |
/public/* |
Public endpoints (no session) |
/health, /docs, /openapi.json |
Infrastructure endpoints |
Defense in Depth
CSRF protection operates on top of SameSite=strict cookies. Even if a browser bug bypasses SameSite, the CSRF token provides a second line of defense. Conversely, even if CSRF token validation has a flaw, SameSite prevents cross-origin cookie submission.
API Key Authentication¶
For programmatic access (scripts, integrations), 4pass supports API key authentication as an alternative to JWT sessions.
| Property | Detail |
|---|---|
| Generation | secrets.token_urlsafe(32) — 256 bits of entropy |
| Storage | SHA-256 hash only — plaintext is shown once at creation and never stored |
| Lookup | Hash incoming key, query api_keys table |
| Expiration | Configurable per key |
| Tracking | last_used_at updated on each use |
| Revocation | Immediate via dashboard or API |
Authorization: Bearer <api_key>
│
├── SHA-256(api_key) → lookup in api_keys table
├── Check expiration
├── Update last_used_at
└── Resolve user → proceed
CAPTCHA¶
4pass uses Cloudflare Turnstile — an invisible, privacy-preserving CAPTCHA that requires no user interaction.
| Property | Detail |
|---|---|
| Provider | Cloudflare Turnstile |
| UX impact | Zero — invisible, no puzzles or checkboxes |
| Applied to | /auth/login, /auth/register |
| Verification | Server-side via Cloudflare API (siteverify endpoint) |
| Configuration | CAPTCHA_ENABLED environment variable |
| Scanner bypass | Security scanners (Wapiti, OWASP ZAP) detected via User-Agent can bypass for testing |
sequenceDiagram
participant Browser
participant Turnstile as Cloudflare Turnstile
participant API as FastAPI
participant CF as Cloudflare API
Browser->>Turnstile: Load invisible widget
Turnstile-->>Browser: Challenge token (automatic)
Browser->>API: POST /auth/login<br/>{email, password, turnstile_token}
API->>CF: POST siteverify<br/>{secret, token, ip}
CF-->>API: {success: true}
API->>API: Proceed with login
IP Whitelist¶
Users can optionally restrict trading endpoints to specific IP addresses:
- Configured per-user via the dashboard settings
- Stored in
user_allowed_ipstable - Checked on webhook and trading requests
- Violation logging:
webhook_ip_blockedaudit event with full request context - Email alerts: Real-time notification when a request is blocked by the whitelist (rate-limited to 1 per 30 minutes)
When to Use IP Whitelisting
IP whitelisting is recommended for users who send webhooks from a fixed TradingView IP or a dedicated server. It adds a network-level restriction that operates independently of all other authentication layers.
Middleware Stack¶
Security middleware executes on every request in a fixed order. Each layer applies globally — no route can accidentally bypass protection.
flowchart LR
REQ["Incoming<br/>Request"] --> CORS["1. CORS<br/>Origin Validation"]
CORS --> BROWSER["2. BrowserOnly<br/>Blocks curl/Postman"]
BROWSER --> CSRF["3. CSRF<br/>Token Validation"]
CSRF --> HEADERS["4. Security Headers<br/>HSTS, CSP, X-Frame"]
HEADERS --> APP["Application<br/>Route Handler"]
1. CORS Middleware¶
| Property | Value |
|---|---|
| Allowed origins | Production domain + localhost:5173 (dev) via ALLOWED_ORIGINS env |
| Allow credentials | true (required for HttpOnly cookie transmission) |
| Allowed methods | All standard HTTP methods |
| Expose headers | Limited set |
2. BrowserOnly Middleware¶
Rejects non-browser requests in production by validating Origin and Referer headers that browsers send automatically.
Excluded from check: /webhook/*, /public/*, /setup/*, /health, /static/*, /docs, landing pages, SEO files.
3. CSRF Middleware¶
Extracts X-CSRF-Token header on state-changing methods (POST, PUT, DELETE, PATCH) and stores it in request.state for downstream validation against the session token.
4. Security Headers Middleware¶
Injects security response headers on every response (replaces NGINX security-headers.conf when running behind ALB):
| Header | Value | Purpose |
|---|---|---|
Strict-Transport-Security |
max-age=31536000; includeSubDomains; preload |
Force HTTPS for 1 year |
Content-Security-Policy |
Restrictive policy with explicit allowlists | Prevent XSS, data injection |
X-Content-Type-Options |
nosniff |
Prevent MIME sniffing |
X-Frame-Options |
SAMEORIGIN |
Prevent clickjacking |
X-XSS-Protection |
1; mode=block |
Legacy XSS filter |
Cross-Origin-Opener-Policy |
same-origin |
Spectre mitigation |
Cross-Origin-Resource-Policy |
same-origin |
Spectre mitigation |
Referrer-Policy |
strict-origin-when-cross-origin |
Limit referrer leakage |
Permissions-Policy |
geolocation=(), microphone=(), camera=(), payment=() |
Disable unused APIs |