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.
Key security properties
JWT tokens live exclusively in HttpOnly cookies — JavaScript never sees, stores, or transmits tokens, eliminating XSS-based token theft. Every state-changing request requires a per-session CSRF token via the Synchronizer Token Pattern, validated with constant-time comparison. SameSite=strict on all cookies prevents cross-origin submission even if a browser bug bypasses CSRF.
JWT Token Architecture¶
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:
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 |
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.
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 |