MeshDash Docs
R2.0
/
Home Security Authentication & Security

Authentication & Security

Security auth security login jwt csrf totp mfa 2fa bcrypt roles admin operator spectator user management session brute force headers cookies
Complete reference for MeshDash authentication — JWT-in-cookie auth model, login/MFA flows, CSRF protection, user roles, session management, brute-force protection, security headers, and first-run setup.

MeshDash uses a JWT-in-cookie authentication model with optional TOTP-based MFA. There is no refresh-token endpoint — sessions are long-lived JWTs that auto-renew on status polling when less than half their lifetime remains.

Public Mode note: By default, MeshDash runs in PUBLIC_MODE=True (no auth required). Set PUBLIC_MODE=False and create users via the Setup Wizard or C2 provisioning to activate authentication.
This page covers the full security architecture. For the raw API endpoints — login/logout, token lifecycle, and how to call them from scripts — see API — Authentication.

Architecture Overview

ComponentFilePurpose
Auth corecore/auth.pyJWT creation/verification, password hashing, TOTP/backup-code verification, CSRF generation
Auth routescore/routes/auth_routes.pyLogin/logout, TOTP setup/confirm/disable, account info
Web routescore/routes/web_routes.pyLogin page serving, login POST with brute-force protection, TOTP verification
Admin routescore/routes/admin_routes.pyUser CRUD, role/suspend/MFA management, initial setup
Middlewarecore/middleware.pySecurity headers, CSRF cookie injection, login lockout
Databasecore/database.pyUser persistence, TOTP state, backup codes

Login Flow — Standard (No MFA)

Browser                    Server
  |                          |
  |  GET /login              |
  |  <--- login.html --------|
  |                          |
  |  POST /login             |
  |  {username, password}    |
  |                          |
  |  (validate credentials)  |
  |  (check brute-force)     |
  |  (check disabled)        |
  |  (check MFA flags)       |
  |                          |
  |  <-- 302 redirect / -----|  + Set-Cookie: access_token=Bearer <jwt>; httponly; samesite=strict
  |                          |
  1. POST /login with username + password (form-encoded).
  2. Server checks brute-force lockout (_check_login_not_locked).
  3. Looks up user in DB, verifies bcrypt hash.
  4. Checks disabled flag → returns error if suspended.
  5. Checks totp_enabled → if true, enters MFA flow.
  6. Checks must_setup_mfa or force_mfa && !totp_enabled → if true, enters forced MFA setup.
  7. Otherwise: records login, clears lockout, creates JWT, sets cookie, redirects to /.

Login Flow — MFA (TOTP)

Browser                    Server
  |                          |
  |  POST /login             |
  |  {username, password}    |
  |                          |
  |  <-- 200 JSON -----------|  {mfa_required: true, preauth_token: <jwt>}
  |                          |
  |  POST /login/verify-totp |
  |  {preauth_token, totp_code}
  |                          |
  |  (validate preauth token)|
  |  (verify TOTP or backup) |
  |                          |
  |  <-- 200 JSON -----------|  {success: true, redirect: "/"} + Set-Cookie: access_token + csrf-token
  1. After successful password check, if totp_enabled, server returns a preauth token (90s JWT, claim preauth: true) instead of the session cookie.
  2. Frontend prompts for 6-digit TOTP code.
  3. POST /login/verify-totp with preauth_token + totp_code.
  4. Server validates preauth token, verifies TOTP code (with 1-window clock drift tolerance), or falls back to backup code verification.
  5. On success: records login, creates session JWT, sets access_token + csrf-token cookies, returns JSON success.

Forced MFA Setup

When a user has must_setup_mfa=True or force_mfa=True and TOTP is not yet enabled:

  1. After password check, server returns {mfa_setup_required: true, preauth_token}.
  2. Frontend enters MFA setup flow using preauth token.
  3. POST /login/setup-totp → generates secret + QR code (base64 PNG) and manual-entry key.
  4. POST /login/confirm-totp-setup → verifies code, enables TOTP, clears must_setup_mfa, issues backup codes, sets session cookie.
  5. User cannot access the dashboard until MFA is configured.

Decision Tree: Login / MFA / Forced MFA Flows

User posts username + password
  │
  ├─ Disabled? → "Account suspended. Contact your administrator."
  ├─ Brute-force locked? → "Too many login attempts. Try again in X minutes."
  ├─ Password wrong? → "Invalid Credentials" (no username existence leak)
  │
  ├─ Password accepted:
  │   ├─ must_setup_mfa or (force_mfa && !totp_enabled)?
  │   │   → MFA SETUP FLOW: Setup TOTP → Confirm → Session
  │   ├─ totp_enabled?
  │   │   → MFA VERIFY FLOW: Enter TOTP or backup code → Session
  │   └─ Neither?
  │       → STANDARD LOGIN: Session cookie set → Redirect

JWT Token Structure

Session Token

{
  "sub": "<username>",       // subject = username
  "exp": <expiry_timestamp>, // UTC epoch
  "iat": <issued_at>         // UTC epoch
}
  • Algorithm: HS256
  • Signing key: AUTH_SECRET_KEY (generated via secrets.token_hex(32) by default)
  • Default expiry: AUTH_TOKEN_EXPIRE_MINUTES = 10080 (7 days)

Preauth Token

{
  "sub": "<username>",
  "preauth": true,
  "exp": <now + 90 seconds>,
  "iat": <now>
}
  • Lifetime: 90 seconds
  • Purpose: bridges the gap between password validation and TOTP verification

CSRF Protection — Double-Submit Cookie Pattern

Cookie
csrf-token — generated via secrets.token_urlsafe(32), auto-set by middleware if missing. Flags: httponly; samesite=strict; path=/.
HTML injection
<meta name="csrf-token" content="<token>"> + window._csrfToken JS global.
Verification
Cookie value must match x-csrf-token header on all state-changing requests. Returns 403 on mismatch or missing.

Protected endpoints include all admin user management, TOTP setup/confirm/disable, and password changes.

TOTP/MFA System

Setup Flow

POST /api/totp/setup        →  {secret, qr_code (data:image/png;base64,...), provisioning_uri}
POST /api/totp/confirm      →  {enabled: true, backup_codes: [...], message}
  1. Setup generates pyotp.random_base32() secret, stores in DB.
  2. Confirm verifies the first TOTP code. If valid, generates 8 backup codes (bcrypt-hashed for storage), enables TOTP, returns plaintext codes (shown once).

Backup Codes

  • Count: 8 codes by default.
  • Format: 8-char uppercase hex strings, e.g. A1B2C3D4.
  • Storage: bcrypt-hashed JSON array in backup_codes column.
  • Consumption: atomically consumed — code removed from stored list on use.

Prerequisites

pip install pyotp qrcode[pil]

If not installed, _HAS_TOTP = False and all TOTP endpoints return 501.

User Roles

RoleIDLabelCapabilities
Admin0AdminFull system access: user management, config, MFA enforcement, C2 access, all data
Operator1OperatorStandard dashboard access: messages, node config, map, packets (default)
Spectator2SpectatorRead-only access to dashboard views

User Management (Admin Only)

MethodEndpointDescription
GET/api/usersList all users with full details
POST/api/usersCreate user. Body: {username, password, role, force_mfa}. Min 2 char username, min 6 char password.
PUT/api/users/{username}/roleChange role. Body: {role: 0|1|2}
PUT/api/users/{username}/passwordReset password. Body: {password}
PUT/api/users/{username}/suspendSuspend/reactivate. Body: {suspended: true|false}. Cannot suspend self.
PUT/api/users/{username}/force-mfaForce MFA. Body: {force_mfa: true|false}. Sets must_setup_mfa=true if unset.
DELETE/api/users/{username}/mfaReset MFA for a user. Disables TOTP, clears backup codes.
DELETE/api/users/{username}Delete user permanently. Cannot delete self.
POST/api/users/generate-passwordGenerate a random 22-char password.

Session Management

  • Auto-renewal: GET /api/status checks remaining token lifetime. If below half-life (default: <3.5 days for 7-day token), a new JWT is issued.
  • No server-side session store: JWTs are valid until expiry. Suspending/deleting a user prevents new token issuance but does not invalidate existing tokens.
  • Logout: GET /logout deletes access_token cookie and redirects to /login.

Brute-Force Protection

ParameterValue
Max login attempts5 (_MAX_LOGIN_ATTEMPTS)
Lockout duration300 seconds / 5 minutes (_LOCKOUT_SECONDS)
ScopePer-username, in-memory (lost on server restart)

Login errors return generic "Invalid Credentials" — no username existence leak.

First-Run Setup

On first boot with no users:

  1. C2-provisioned: If INITIAL_ADMIN_USERNAME + INITIAL_ADMIN_PASSWORD are in config (set by C2 wizard), admin user is created and credentials are removed from config.
  2. Interactive setup: Otherwise, setup.flag is created. GET /setup serves the Setup Wizard UI. POST /api/system/config/initial-setup creates the first admin atomically.
  3. There are no hardcoded credentials — the system starts empty and requires explicit admin creation.

    Security Headers

    Set by _security_headers middleware on every response:

    Content-Security-Policy
    Restrictive policy with CDN allowances for required third-party resources
    Cache-Control
    no-store, no-cache, must-revalidate, proxy-revalidate
    X-Content-Type-Options
    nosniff
    Strict-Transport-Security
    max-age=31536000; includeSubDomains
    X-Frame-Options
    SAMEORIGIN
    Referrer-Policy
    strict-origin-when-cross-origin
    X-Robots-Tag
    noindex, nofollow
    Permissions-Policy
    camera=(), microphone=(), geolocation=()

    Cookie Settings

    CookieFlagsSet By
    access_tokenhttponly, samesite=strictLogin, setup, TOTP verify endpoints
    csrf-tokenhttponly, samesite=strict, path=/Security middleware (auto-inject if missing), login responses
    The secure flag is not set on cookies — tokens are sent over plain HTTP for LAN deployments. If exposing to the internet, add a reverse proxy with HTTPS.

    Public Mode

    SettingPUBLIC_MODE=True (default)PUBLIC_MODE=False
    AuthenticationNo auth required — everyone has accessJWT authentication active
    Login pageGET /login → 302 redirect to /Login page served
    User managementNot availableAvailable to Admins
    Setup wizardSkippedActive until first user created
    DatabaseIn-memory (:memory:), ephemeralPersistent SQLite

    Auth API Endpoints Reference

    Login / Logout

    MethodEndpointAuthCSRFDescription
    GET/loginNoNoHTML login page (302 redirects if PUBLIC_MODE)
    POST/loginNoNoForm: username, password. Returns 302 + cookie, or 200 {mfa_required, preauth_token}
    POST/login/verify-totpNoNoForm: preauth_token, totp_code. Returns 200 + cookies, or 401
    POST/login/setup-totpNoNoForm: preauth_token. Returns {secret, qr_code}
    POST/login/confirm-totp-setupNoNoForm: preauth_token, totp_code. Returns {success, backup_codes} + cookie
    GET/logoutNoNoDeletes cookie, redirects to /login

    Account / MFA

    MethodEndpointAuthCSRFDescription
    GET/api/account/meYesNoUser profile: username, role, totp_enabled, force_mfa, backup_codes_remaining, last_login, login_count, created_at
    GET/api/totp/statusYesNo{totp_available, totp_enabled, backup_codes_remaining}
    POST/api/totp/setupYesYes{secret, qr_code, provisioning_uri}
    POST/api/totp/confirmYesYesForm: code. Returns {enabled, backup_codes, message}
    POST/api/totp/disableYesYesForm: password. Returns {enabled: false, message}

    User Management (Admin Only)

    MethodEndpointCSRFDescription
    GET/api/usersNoList all users with full details
    POST/api/usersYesJSON: {username, password, role, force_mfa}
    PUT/api/users/{username}/roleYesJSON: {role: 0|1|2}
    PUT/api/users/{username}/passwordYesJSON: {password}
    PUT/api/users/{username}/suspendYesJSON: {suspended: true|false}. Cannot suspend self.
    PUT/api/users/{username}/force-mfaYesJSON: {force_mfa: true|false}
    DELETE/api/users/{username}/mfaYesReset MFA — disables TOTP, clears backup codes
    DELETE/api/users/{username}YesDelete user permanently. Cannot delete self.
    POST/api/users/generate-passwordYesGenerates random 22-char password

    Setup

    MethodEndpointAuthCSRFDescription
    GET/setupNoNoHTML setup wizard (only if no users exist)
    POST/api/system/config/initial-setupNoNoJSON: {adminUser: {username, password}, configValues: {...}, rawSelections: {...}}. Atomic user creation — no race condition on concurrent setups.

    C2 Bridge Token (Internal)

    The C2 bridge uses an internal service token for proxied remote requests. Requests from the C2 relay arrive with a short-lived (30-second) internal JWT signed with AUTH_SECRET_KEY. The sub claim is "__c2_bridge__" with "internal": true. This token is validated by get_current_active_user() which returns a synthetic user object — no DB lookup, no disable mechanism. This is a backdoor-by-design for the C2 proxy system.

    Security Observations

    • No secure cookie flag — tokens transmitted over plain HTTP on LAN. Acceptable for local deployments; add a reverse proxy with HTTPS if exposed to the internet.
    • No session revocation — JWTs are valid until expiry. Suspending a user doesn't invalidate already-issued tokens.
    • Brute-force protection is in-memory — lockout state is lost on server restart.
    • Preauth tokens are not single-use — 90s window allows replay, but short lifetime mitigates this.
    • unsafe-inline in CSP — required for current inline script/style patterns. Limits XSS protection.
    • Backup codes are consumed atomically — no race condition on concurrent use.
    • Initial setup is race-safeatomic_setup_user uses a DB transaction to count+insert, preventing two concurrent setups from creating duplicate admins.

    Database Schema — Users Table

    CREATE TABLE users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        username TEXT UNIQUE NOT NULL,
        hashed_password TEXT,
        disabled BOOLEAN DEFAULT FALSE,
        role INTEGER DEFAULT 1,              -- 0=Admin, 1=Operator, 2=Spectator
        force_mfa BOOLEAN DEFAULT FALSE,      -- admin-enforced MFA requirement
        must_setup_mfa BOOLEAN DEFAULT FALSE,  -- pending MFA setup flag
        totp_secret TEXT DEFAULT NULL,         -- base32 TOTP secret
        totp_enabled BOOLEAN DEFAULT FALSE,    -- MFA active
        backup_codes TEXT DEFAULT NULL,        -- JSON array of bcrypt-hashed backup codes
        last_login DATETIME DEFAULT NULL,     -- timestamp of last successful login
        login_count INTEGER DEFAULT 0,        -- total successful logins
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    );

    See also: Radio Connection for connection security. Configuration Reference for auth-related config keys.