- /map/data: resolve viewer OIDC session and apply the endpoint access
mapping before fetching profiles; serve Cache-Control: private when
OIDC is enabled (payload varies by viewer)
- /user/profile/me + /user/profile/{id}: trust X-User-Id only with a
valid API key (new optional_user_identity dependency); closes the
OIDC subject-identifier leak via forged headers
- OIDC_SESSION_SECRET is now mandatory when OIDC is enabled (hard fail,
no insecure-dev-secret fallback)
- /metrics denies unauthenticated access when no API read key is set;
new METRICS_PUBLIC opt-in (Settings, CLI, compose, env factory)
- CORS: unset CORS_ORIGINS keeps wildcard compat but drops
allow_credentials (invalid combination in browsers) + warns
- Web tier: SecurityHeadersMiddleware (nosniff, X-Frame-Options DENY,
Referrer-Policy, Permissions-Policy) + nonce-based CSP; nonce applied
to spa.html inline scripts; WEB_SECURITY_HEADERS kill switch and
WEB_CSP_EXTRA for custom tile hosts; API emits nosniff
Breaking changes documented in docs/upgrading.md.
7.0 KiB
OIDC Authentication
MeshCore Hub supports OpenID Connect (OIDC) for authenticating web dashboard users. When enabled, the web dashboard uses role-based access control to gate API endpoints through the proxy layer.
When OIDC is disabled (the default), the web proxy only allows read (GET) access to API endpoints. Write operations (POST/PUT/DELETE) and unknown endpoints are blocked. Admin operations must be performed via the CLI or direct API access with Bearer tokens.
Architecture
OIDC is implemented entirely in the web dashboard layer. The REST API uses static Bearer token authentication independently of OIDC. The web dashboard proxies API requests and gates access using a per-endpoint, per-method role mapping (ENDPOINT_ACCESS in web/app.py).
Browser → Web Dashboard (OIDC session + API proxy) → REST API (Bearer token)
Role-Based Access
User roles are read from the OIDC token's roles claim (configurable via OIDC_ROLES_CLAIM). The web proxy checks these roles against the ENDPOINT_ACCESS mapping to determine which API endpoints and HTTP methods each user can access.
Defined roles:
| Role | Config Variable | Default | Description |
|---|---|---|---|
| Admin | OIDC_ROLE_ADMIN |
admin |
Full write access to all API endpoints through the proxy |
| Operator | OIDC_ROLE_OPERATOR |
operator |
Manage nodes, node tags, adoptions, and routes (create; edit/delete only routes they created) |
| Member | OIDC_ROLE_MEMBER |
member |
Read-only access (no endpoint assignments) |
The role names are configurable to match your IdP's role naming convention. For example, if your IdP uses superuser instead of admin, set OIDC_ROLE_ADMIN=superuser.
Additional roles can be added to the ENDPOINT_ACCESS mapping in src/meshcore_hub/web/app.py and the corresponding OIDC_ROLE_* config variable.
Endpoint Access Mapping
The proxy uses a hardcoded per-endpoint, per-method mapping in src/meshcore_hub/web/app.py:
| Path prefix | Method | Access |
|---|---|---|
v1/nodes |
GET | Open |
v1/nodes/ |
GET | Open |
v1/nodes/ |
POST, PUT, DELETE | admin, operator |
v1/members |
GET | Open |
v1/members |
POST, PUT, DELETE | admin |
v1/messages |
GET | Open |
v1/advertisements |
GET | Open |
v1/adoptions |
POST, DELETE | admin, operator |
v1/routes |
POST | admin, operator |
v1/routes/ |
PUT, DELETE | admin, operator |
v1/dashboard |
GET | Open |
v1/trace-paths |
GET | Open |
v1/telemetry |
GET | Open |
- Open = no authentication required (anonymous OK, works with or without OIDC)
admin= requires OIDC enabled + user has theadminroleadmin,operator= requires OIDC enabled + user has theadminoroperatorrole- Method not listed for a matched prefix = denied
- No prefix match = denied
Client-Side Role Checks
The SPA receives the user's roles array in window.__APP_CONFIG__.roles. Client-side pages use the hasRole(roleName) helper, which returns true when OIDC is disabled (open access) or when the user has the specified role.
Trust Model for Identity Headers
When the web proxy forwards a request to the API, it injects X-User-Id, X-User-Name, and X-User-Roles headers derived from the OIDC session, alongside the API bearer key. These headers are only honoured by the API when a valid API key accompanies them (require_user_owner / optional_user_identity in api/auth.py) — client-supplied identity headers are dropped by the proxy and, with API keys configured, a bare X-User-Id sent directly to the API port is rejected. Keep the API port unreachable from untrusted networks; the API key is the trust anchor for proxy-injected identity.
Server-side aggregations (e.g. /map/data) resolve the viewer's session and apply the same endpoint access mapping as the proxy, so tightening v1/user/profiles in ENDPOINT_ACCESS also removes profile data from /map/data for viewers who would be denied.
Session Security
Sessions are signed with OIDC_SESSION_SECRET using a SameSite=Lax cookie (meshcore-session). The web service refuses to start when OIDC_ENABLED=true and no secret is set — there is no insecure fallback. Generate one with python -c "import secrets; print(secrets.token_urlsafe(48))". Set OIDC_COOKIE_SECURE=true in production so cookies are restricted to HTTPS.
Login Flow
- User visits
/admin/— no session exists — server redirects to/auth/login?next=/admin/ /auth/loginstores thenextURL in the session and redirects to the IdP- User authenticates at the IdP, which redirects back to
/auth/callback?code=... /auth/callbackexchanges the authorization code for tokens, extracts userinfo and roles, stores them in a session cookie, and redirects to thenextURL- The SPA receives
window.__APP_CONFIG__withoidc_enabled,user, androlesflags - Admin routes are registered client-side only when
hasRole('admin')istrue - Write operations through the API proxy are checked against the
ENDPOINT_ACCESSmapping - Logout (
/auth/logout) clears the session and redirects to the IdP's end-session endpoint
Configuration
All OIDC settings are environment variables. Set OIDC_ENABLED=true to activate. For the full variable reference (OIDC_CLIENT_ID, OIDC_SCOPES, OIDC_ROLE_*, OIDC_SESSION_*, etc.), see configuration.md → Auth.
Local Development (No HTTPS)
To test OIDC locally without TLS:
- Set
OIDC_REDIRECT_URI=http://localhost:8080/auth/callbackexplicitly — auto-derivation may produce an incorrect URL depending on your setup - Keep
OIDC_COOKIE_SECURE=false(the default) — cookies won't be restricted to HTTPS - Register
http://localhost:8080/auth/callbackas a redirect URI in your IdP - Register
http://localhost:8080/as a post-logout redirect URI in your IdP OIDC_SESSION_SECRETis still required — generate one even for local testing
IdP Provider Guides
LogTo
LogTo is an open-source identity provider that works well with MeshCore Hub.
Setup:
- Create a new Traditional Web application in LogTo
- Set the redirect URI to
https://your-hub-domain/auth/callback - Set the post-logout redirect URI to
https://your-hub-domain/ - Copy the client ID and client secret into
OIDC_CLIENT_IDandOIDC_CLIENT_SECRET - Set
OIDC_DISCOVERY_URLto your LogTo endpoint (e.g.https://auth.example.com/oidc)
Required scope changes:
LogTo returns roles via the roles claim in the ID token. You must include roles in the scopes:
OIDC_SCOPES="openid email profile roles"
OIDC_ROLES_CLAIM defaults to roles, which matches LogTo's claim name — no change needed.
Role assignment:
Create an admin role in LogTo and assign it to users who should have write access through the web dashboard. The role name must match OIDC_ROLE_ADMIN (default: admin). If your LogTo setup uses a different role name, set OIDC_ROLE_ADMIN accordingly.