diff --git a/.env.example b/.env.example index a1688fa..bfc2c85 100644 --- a/.env.example +++ b/.env.example @@ -23,12 +23,6 @@ # # Serial ports are typically /dev/ttyUSB[0-9] or /dev/ttyACM[0-9] on Linux. # ----------------------------------------------------------------------------- -# NOTE: If using direnv, values with spaces MUST be quoted: -# OIDC_SCOPES="openid email profile" -# The application strips surrounding quotes automatically. Do NOT use -# quotes for values loaded via Docker Compose map syntax: -# environment: -# OIDC_SCOPES: openid email profile # ----------------------------------------------------------------------------- # ============================================================================= @@ -359,7 +353,7 @@ WEB_PORT=8080 # OIDC client secret (from your IdP) # OIDC_CLIENT_SECRET= -# OIDC discovery URL (your IdP's .well-known/openid-configuration endpoint) +# OIDC discovery base URL (.well-known/openid-configuration is appended automatically) # OIDC_DISCOVERY_URL= # OIDC callback URL (overrides auto-derivation from request) diff --git a/AGENTS.md b/AGENTS.md index 66b651d..6c67934 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ This document provides context and guidelines for AI coding assistants working o * You MUST install all project dependencies using `pip install -e ".[dev]"` command` * You MUST install `pre-commit` for quality checks * **Never `git push` without explicit confirmation** — staging and committing after discrete changes is fine, but pushing to remote requires the user to explicitly request it -* You MUST keep project documentation in sync with behavior/config/schema changes made in code (at minimum update relevant sections in `README.md`, `SCHEMAS.md`, `docs/upgrading.md`, `docs/letsmesh.md` when applicable) +* You MUST keep project documentation in sync with behavior/config/schema changes made in code (at minimum update relevant sections in `README.md`, `SCHEMAS.md`, `docs/upgrading.md`, `docs/auth.md`, `docs/letsmesh.md` when applicable) * Before commiting: - Run **targeted tests** for the components you changed, not the full suite: - `pytest tests/test_web/` for web-only changes (templates, static JS, web routes) @@ -39,6 +39,7 @@ MeshCore Hub is a Python 3.14+ monorepo for managing and orchestrating MeshCore - [SCHEMAS.md](SCHEMAS.md) - MeshCore event JSON schemas and database mappings - [docs/upgrading.md](docs/upgrading.md) - Upgrade guide for breaking changes +- [docs/auth.md](docs/auth.md) - OIDC authentication setup and configuration - [docs/letsmesh.md](docs/letsmesh.md) - LetsMesh packet decoding details - [docs/seeding.md](docs/seeding.md) - Seed data format and import guide - [docs/i18n.md](docs/i18n.md) - Translation reference guide @@ -643,7 +644,7 @@ Key variables: - `OIDC_DISCOVERY_URL` - OIDC discovery URL (required if OIDC_ENABLED=true) - `OIDC_REDIRECT_URI` - Explicit callback URL (overrides auto-derivation) - `OIDC_POST_LOGOUT_REDIRECT_URI` - Post-logout redirect URI (must match IdP sign-out URIs, falls back to `OIDC_REDIRECT_URI` base) -- `OIDC_SCOPES` - OAuth scopes (default: `openid email profile`). The `openid` scope is required for ID tokens and userinfo. Quotes are stripped automatically for direnv compatibility. When using LogTo as the OIDC provider, include `roles` in `OIDC_SCOPES` (e.g., `"openid email profile roles"`) to enable role-based admin access. +- `OIDC_SCOPES` - OAuth scopes (default: `openid email profile`). The `openid` scope is required for ID tokens and userinfo. Quotes are stripped automatically. When using LogTo as the OIDC provider, include `roles` in `OIDC_SCOPES` (e.g., `"openid email profile roles"`) to enable role-based admin access. - `OIDC_ROLES_CLAIM` - ID token claim for roles (default: `roles`) - `OIDC_ADMIN_ROLE` - Role value for admin access (default: `admin`) - `OIDC_MEMBER_ROLE` - Role value for member access (default: `member`) diff --git a/README.md b/README.md index ed78097..f3f6c72 100644 --- a/README.md +++ b/README.md @@ -372,7 +372,7 @@ The collector automatically cleans up old event data and inactive nodes: | `OIDC_ENABLED` | `false` | Enable OIDC authentication for the web dashboard | | `OIDC_CLIENT_ID` | _(none)_ | OIDC client ID (from IdP, required when OIDC_ENABLED=true) | | `OIDC_CLIENT_SECRET` | _(none)_ | OIDC client secret (from IdP, required when OIDC_ENABLED=true) | -| `OIDC_DISCOVERY_URL` | _(none)_ | IdP's `.well-known/openid-configuration` URL (required when OIDC_ENABLED=true) | +| `OIDC_DISCOVERY_URL` | _(none)_ | IdP base URL — `.well-known/openid-configuration` is appended automatically (required when OIDC_ENABLED=true) | | `OIDC_REDIRECT_URI` | _(auto-derived)_ | Explicit callback URL (overrides auto-derivation from request) | | `OIDC_POST_LOGOUT_REDIRECT_URI` | _(auto-derived)_ | Post-logout redirect URI (must match Sign-out redirect URIs in IdP). Falls back to `OIDC_REDIRECT_URI` base or `request.base_url` | | `OIDC_SCOPES` | `openid email profile` | OAuth scopes to request. The `openid` scope is required for ID tokens. Quotes are stripped automatically. | @@ -449,6 +449,8 @@ curl -H "Authorization: Bearer " http://localhost:8000/api/v1/node curl -H "Authorization: Bearer " http://localhost:8000/api/v1/members ``` +The web dashboard supports OIDC/OAuth2 authentication for admin access. When enabled, users must authenticate with an identity provider and have the `admin` role assigned. See [docs/auth.md](docs/auth.md) for setup instructions and IdP-specific guides. + ### Example Endpoints | Method | Endpoint | Description | @@ -597,6 +599,7 @@ meshcore-hub/ - [docs/seeding.md](docs/seeding.md) - Seed data format and import guide - [docs/i18n.md](docs/i18n.md) - Translation reference guide - [docs/content.md](docs/content.md) - Custom content setup guide +- [docs/auth.md](docs/auth.md) - OIDC authentication setup and configuration - [docs/webhooks.md](docs/webhooks.md) - Webhook configuration reference - [AGENTS.md](AGENTS.md) - Guidelines for AI coding assistants diff --git a/docs/auth.md b/docs/auth.md new file mode 100644 index 0000000..10c379b --- /dev/null +++ b/docs/auth.md @@ -0,0 +1,82 @@ +# OIDC Authentication + +MeshCore Hub supports OpenID Connect (OIDC) for authenticating web dashboard users. When enabled, the admin interface (`/admin/`) requires an authenticated session with the `admin` role. + +When OIDC is **disabled** (the default), no admin functionality is exposed — the admin link is hidden from the navbar and admin routes are not registered. + +## 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 write operations (POST, PUT, DELETE, PATCH) by checking the user's session role. + +``` +Browser → Web Dashboard (OIDC session + API proxy) → REST API (Bearer token) +``` + +## Login Flow + +1. User visits `/admin/` — no session exists — server redirects to `/auth/login?next=/admin/` +2. `/auth/login` stores the `next` URL in the session and redirects to the IdP +3. User authenticates at the IdP, which redirects back to `/auth/callback?code=...` +4. `/auth/callback` exchanges the authorization code for tokens, extracts userinfo and roles, stores them in a session cookie, and redirects to the `next` URL +5. The SPA receives `window.__APP_CONFIG__` with `oidc_enabled`, `user`, `is_member`, and `is_admin` flags +6. Admin routes are registered client-side only when `is_admin` is `true` +7. Write operations through the API proxy are blocked for non-admin sessions (HTTP 403) +8. 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. + +| Variable | Default | Description | +|----------|---------|-------------| +| `OIDC_ENABLED` | `false` | Enable OIDC authentication | +| `OIDC_CLIENT_ID` | _(none)_ | OAuth2 client ID from your IdP | +| `OIDC_CLIENT_SECRET` | _(none)_ | OAuth2 client secret from your IdP | +| `OIDC_DISCOVERY_URL` | _(none)_ | IdP base URL — `.well-known/openid-configuration` is appended automatically (e.g. `https://auth.example.com/oidc`) | +| `OIDC_REDIRECT_URI` | _(auto)_ | Override callback URL (auto-derived from request if not set) | +| `OIDC_POST_LOGOUT_REDIRECT_URI` | _(auto)_ | Post-logout redirect URI (falls back to `OIDC_REDIRECT_URI` base or request URL) | +| `OIDC_SCOPES` | `openid email profile` | OAuth scopes to request. The `openid` scope is required. Quotes are stripped automatically. | +| `OIDC_ROLES_CLAIM` | `roles` | ID token claim name containing user roles | +| `OIDC_ADMIN_ROLE` | `admin` | Role value that grants admin access | +| `OIDC_MEMBER_ROLE` | `member` | Role value that grants member access | +| `OIDC_SESSION_SECRET` | _(none)_ | Secret for signing session cookies (generate with `openssl rand -hex 32`) | +| `OIDC_SESSION_MAX_AGE` | `86400` | Session cookie lifetime in seconds (default: 24 hours) | +| `OIDC_COOKIE_SECURE` | `false` | Set to `true` to require HTTPS for session cookies | + +## Local Development (No HTTPS) + +To test OIDC locally without TLS: + +- Set `OIDC_REDIRECT_URI=http://localhost:8080/auth/callback` explicitly — 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/callback` as a redirect URI in your IdP +- Register `http://localhost:8080/` as a post-logout redirect URI in your IdP +- `OIDC_SESSION_SECRET` is still required — generate one even for local testing + +## IdP Provider Guides + +### LogTo + +[LogTo](https://logto.io/) is an open-source identity provider that works well with MeshCore Hub. + +**Setup:** + +1. Create a new **Traditional Web** application in LogTo +2. Set the redirect URI to `https://your-hub-domain/auth/callback` +3. Set the post-logout redirect URI to `https://your-hub-domain/` +4. Copy the client ID and client secret into `OIDC_CLIENT_ID` and `OIDC_CLIENT_SECRET` +5. Set `OIDC_DISCOVERY_URL` to 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: + +```bash +OIDC_SCOPES="openid email profile roles" +``` + +`OIDC_ROLES_CLAIM` defaults to `roles`, which matches LogTo's claim name — no change needed. + +**Role assignment:** + +Create `admin` and/or `member` roles in LogTo and assign them to users. The role names must match `OIDC_ADMIN_ROLE` and `OIDC_MEMBER_ROLE` (both default to `admin` and `member` respectively).