For the complete documentation index, see llms.txt. This page is also available as Markdown.

24 - Browser Security

Overview

OpenAlgo implements browser-side security measures including session management, CSRF protection, secure cookies, and content security policies.

Architecture Diagram

Browser request path: WSGI IP-ban middleware, CSRFProtect, session expiry check, session route, security headers, secure cookies

Session Management

Session Lifecycle

Session lifecycle: password login, optional TOTP, broker auth or resume, active validation per request, expiry at daily boundary or logout clears session

Session Expiry Configuration

is_session_valid() in utils/session.py expires a session when the current IST time is past today's boundary and the login happened before it. app.py also registers a check_session_expiry before_request guard that skips static assets, /assets/, /api/, and the public routes.

Session Validation

PERMANENT_SESSION_LIFETIME is not a fixed 24 hours: handle_auth_success() sets it per login from get_session_expiry_time(), so the cookie expires at the next SESSION_EXPIRY_TIME boundary. The CSRF cookie mirrors the same flags: WTF_CSRF_COOKIE_HTTPONLY, WTF_CSRF_COOKIE_SAMESITE="Lax", WTF_CSRF_COOKIE_SECURE=USE_HTTPS, name from CSRF_COOKIE_NAME (default csrf_token, prefixed __Secure- on HTTPS). CSRF protection itself is toggled by CSRF_ENABLED (default TRUE) and CSRF_TIME_LIMIT is unlimited unless set.

Flag
Purpose

Secure

Only sent over HTTPS, and only enabled when HOST_SERVER is an https:// URL

HttpOnly

Cannot be read by JavaScript

SameSite=Lax

Prevents CSRF in most cases

__Secure- name prefix

Added automatically on HTTPS installs

Password Security

Argon2 Hashing

Hashing lives on the User model in database/user_db.py. The pepper is the full API_KEY_PEPPER value, not a slice of APP_KEY, and PasswordHasher() is used with its library defaults.

Password Requirements

The accepted special characters are exactly !@#$%^&*. On success the second element is None, not a message.

Login Rate Limiting

Configuration

Implementation

The limiter keys on get_remote_address, which is Werkzeug's request.remote_addr, not on the TRUST_PROXY_HEADERS-aware get_real_ip() helper used by the IP-ban and audit paths. POST /auth/login/totp and POST /auth/broker carry the same two login limits.

TOTP Two-Factor Authentication

Setup Flow

TOTP setup: secret generated and encrypted at account setup, QR first shown on the Profile page after login, 2FA enabled via POST /auth/2fa/configure with a valid current code

TOTP Validation

The totp_secret column holds Fernet ciphertext, so pyotp must be handed get_totp_secret() rather than the raw column. See 50-totp-configuration.md for the master switch and the three purpose flags.

Token Revocation

On Logout

The teardown is deliberately conditional. A stale cookie hitting the daily rollover must not revoke a broker token that a second device has already refreshed, so _has_fresher_session() short-circuits the global teardown.

On Session Expiry

React Frontend Security

API Key Handling

AJAX Request Detection

check_session_validity uses its own slightly wider test (it also accepts request.is_json and an application/json content type) to decide between a 401 JSON body and a redirect to the login page.

Security Headers

csp.py installs a single after_request hook that applies both the CSP header and the fixed header set.

REFERRER_POLICY and PERMISSIONS_POLICY are read from the environment but are not listed in .sample.env, so most installs run on the code defaults above.

Content Security Policy

CSP_ENABLED defaults to TRUE. When CSP_REPORT_ONLY=TRUE the policy is emitted as Content-Security-Policy-Report-Only instead. A CSP already set by the route handler is left alone, which is what lets the Remote MCP /oauth/authorize consent page widen form-action to the registered redirect origin.

Several code defaults are narrower than the values shipped in .sample.env, so an install that copies the sample gets the wider policy.

Directive

Code default (csp.py)

.sample.env value

default-src

'self'

same

script-src

'self' https://cdn.socket.io

'self' 'unsafe-inline' https://cdn.socket.io https://static.cloudflareinsights.com

style-src

'self' 'unsafe-inline'

same

img-src

'self' data: blob:

'self' data:

connect-src

'self' wss: ws:

'self' wss: ws: https://cdn.socket.io

font-src

'self'

same

object-src

'none'

same

media-src

'self'

'self' data: https://*.amazonaws.com https://*.cloudfront.net

frame-src

'self'

same

form-action

'self'

same

base-uri

'self'

same

frame-ancestors

'self'

same

child-src, report-uri, and report-to have no code default and are emitted only when the matching env var is set. upgrade-insecure-requests is added only when CSP_UPGRADE_INSECURE_REQUESTS=TRUE, which defaults to FALSE in both code and .sample.env.

CORS

cors.py applies Flask-CORS to r"/api/*" only.

Setting

Code default (cors.py)

.sample.env value

CORS_ENABLED

FALSE

TRUE

CORS_ALLOWED_ORIGINS

unset, so no origins key

http://127.0.0.1:5000

CORS_ALLOWED_METHODS

unset

GET,POST,DELETE,PUT,PATCH

CORS_ALLOWED_HEADERS

unset

Content-Type,Authorization,X-Requested-With

CORS_EXPOSED_HEADERS

unset

empty

CORS_ALLOW_CREDENTIALS

FALSE

FALSE

CORS_MAX_AGE

unset

86400

When CORS_ENABLED is not TRUE, get_cors_config() returns an empty dict and Flask-CORS falls back to its own defaults for /api/*. Every other key is applied only when the env var is present, so an unset variable means "leave it to Flask-CORS", not "deny".

Session Storage

What's Stored

The Flask session is a signed, unencrypted client-side cookie, so everything below is readable by whoever holds the cookie.

What's NOT Stored

  • Passwords (only Argon2 hashes in the DB)

  • API keys (hashed and encrypted in the DB)

  • The broker auth token. handle_auth_success() carries an explicit note that it must never go in the session cookie; get_auth_token() reads the encrypted DB copy instead

  • Raw password-reset tokens. Only their SHA-256 hash is stored in the session

Credential Masking

Display Masking

The suffix is always eight asterisks and no trailing characters are revealed. The fixed length is intentional: it hides the secret's true length, so a screenshot cannot betray which broker a key belongs to, and it bounds the rendered column width. blueprints/broker_credentials.mask_secret mirrors this.

Key Files Reference

File
Purpose

utils/session.py

Session validity, daily expiry, token revocation

utils/auth_utils.py

Password strength, credential masking, AJAX detection, login success handling

database/user_db.py

User model, Argon2 hashing, TOTP

blueprints/auth.py

Auth routes

csp.py

CSP and the fixed security headers

cors.py

CORS configuration for /api/*

app.py

Cookie and CSRF configuration, session-expiry guard

frontend/src/api/

Secure API calls

Last updated

Was this helpful?