API Authentication
MeshDash uses JWT (HS256) tokens stored in an HttpOnly cookie called access_token. All protected endpoints check this cookie. When PUBLIC_MODE=true (the default until setup is complete), authentication is bypassed entirely.
Login
Submit credentials as a standard HTML form POST. On success, the server sets the access_token cookie and redirects to /.
POST /login
Content-Type: application/x-www-form-urlencoded
username=admin%40example.com&password=yourpassword
Success: HTTP 302 → / with Set-Cookie: access_token=Bearer eyJ…
Failure: HTTP 302 → /login?error=Invalid+Credentials
Using the Token in API Calls
The cookie is sent automatically by browsers. For API clients (scripts, curl, Postman) you must send the cookie header explicitly:
# Example with curl — use the token value from the Set-Cookie response
curl -b "access_token=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://device-ip:8000/api/nodes
Authorization: Bearer header support — the token must be in the access_token cookie. This is by design to prevent CSRF token leakage.Token Lifecycle
Tokens expire after AUTH_TOKEN_EXPIRE_MINUTES (default 7 days). The /api/status endpoint silently refreshes the token when less than half its lifetime remains — so any frontend that polls status will keep sessions alive automatically.
Logout
GET /logout
Deletes the access_token cookie and redirects to /login.
Initial Setup (First Boot)
When no users exist in the database, the /setup page is served. It submits to:
POST /api/system/config/initial-setup
Content-Type: application/json
{
"username": "[email protected]",
"password": "securepassword",
"AUTH_SECRET_KEY": "64hexchars...",
"AUTH_TOKEN_EXPIRE_MINUTES": 10080,
"MESHTASTIC_CONNECTION_TYPE": "SERIAL",
"MESHTASTIC_SERIAL_PORT": "/dev/ttyACM0",
"WEBSERVER_HOST": "0.0.0.0",
"WEBSERVER_PORT": 8000,
...
}
What it does:
- Creates the admin user (bcrypt-hashed password stored in SQLite)
- Writes the full config to
.mesh-dash_config - Deletes
static/.newto permanently disable the setup endpoint - Returns a JWT cookie so the user is logged in immediately
- Hot-reloads key globals without a restart
POST /api/system/config/initial-setup when users already exist, the server returns 400 System already initialized and forcibly deletes static/.new.C2 Internal Token
The remote C2 bridge generates short-lived (30 s) internal tokens to make proxy requests to the local API:
{"sub": "__c2_bridge__", "internal": true}
These are valid only for the duration of a single proxy cycle and are never exposed externally. The dependency get_current_active_user recognises and accepts this principal.
Password Storage
Passwords are hashed using bcrypt via passlib[bcrypt] before being stored. They are never stored or logged in plain text. The INITIAL_ADMIN_PASSWORD key is removed from .mesh-dash_config immediately after the admin account is created.
Session Renewal
The /api/status endpoint (which the frontend polls every 30 seconds) checks the token's remaining lifetime. If less than half of AUTH_TOKEN_EXPIRE_MINUTES remains, a fresh token is silently issued and set in the response cookie. The user never needs to log in again as long as they use the dashboard at least once every 3.5 days (with the default 7-day expiry).
Public Mode
When PUBLIC_MODE=true in config (the default state before initial setup is completed), all authentication is bypassed:
- The
get_current_active_userdependency returns a dummyUser(username="public") - The logout nav item is hidden in the sidebar
- All protected API endpoints are accessible without a token
- Databases are ephemeral (
:memory:SQLite) — data is not persisted to disk - The radio still connects and all real-time features work normally
PUBLIC_MODE=true on an internet-exposed instance — there is no authentication whatsoever.Login Redirect
Protected page routes (/, /map, /settings, etc.) check the access_token cookie server-side. If the token is missing, expired, or invalid, the server returns HTTP 302 → /login. The browser follows the redirect and shows the login page. There is no AJAX-based auth check on these page routes — the redirect is handled entirely server-side.
Multi-User Support
The users table supports multiple accounts. All users have equal access — there are no role distinctions beyond disabled flag. To create additional users after initial setup, use the sqlite3 CLI to insert a bcrypt-hashed password directly, or add a user creation API endpoint via a plugin.
# Generate a bcrypt hash for a new user (Python)
python3 -c "from passlib.context import CryptContext; print(CryptContext(['bcrypt']).hash('newpassword'))"
# Insert into DB
sqlite3 meshtastic_data.db "INSERT INTO users (username, hashed_password) VALUES ('[email protected]', '\$2b\$12\$...');"
Token Security Notes
- Tokens are HS256 JWT signed with
AUTH_SECRET_KEY - The cookie is
httponly=True— JavaScript cannot read it, preventing XSS token theft - The cookie is
samesite="lax"— protects against CSRF for state-changing requests - Changing
AUTH_SECRET_KEYinstantly invalidates all existing sessions (all users are logged out) - The C2 bridge uses a separate short-lived (30 s) internal token that is never sent to the browser