diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3eac82 --- /dev/null +++ b/README.md @@ -0,0 +1,180 @@ +# discourse-oauth2-bridge + +A lightweight Node.js bridge that allows [Authentik](https://goauthentik.io/) (and other OIDC-compatible identity providers) to use [Discourse](https://discourse.org/) as its authentication backend via [DiscourseConnect](https://meta.discourse.org/t/discourseconnect-official-sso-solution-for-discourse/13045). + +## The problem it solves + +Discourse has a built-in SSO provider called DiscourseConnect, which lets other apps authenticate against it. However, most modern apps (Nextcloud, Outline, Invoice Ninja, etc.) expect **OIDC or OAuth2**, not DiscourseConnect. This bridge sits between Authentik and Discourse, translating DiscourseConnect into a standard OAuth2/OIDC interface that Authentik can consume. + +``` +App (e.g. Nextcloud, Outline) + │ + │ OIDC + ▼ + Authentik + │ + │ OAuth2 (via this bridge) + ▼ +discourse-oauth2-bridge ←── this repo + │ + │ DiscourseConnect + ▼ + Discourse ←── source of truth: users, groups, permissions +``` + +User accounts, group memberships and permissions are all managed in Discourse. Any app connected via Authentik inherits them automatically. + +## How it works + +The bridge implements a minimal OAuth2 authorization code flow: + +1. **`/authorize`** — receives an OAuth2 authorization request from Authentik, generates a DiscourseConnect SSO request, and redirects the user to Discourse to log in +2. **`/callback`** — Discourse redirects back here after login; the bridge validates the signature, extracts user info (username, email, groups, admin status), and issues a short-lived authorization code +3. **`/token`** — exchanges the authorization code for an access token +4. **`/userinfo`** — returns the authenticated user's profile to Authentik in OIDC-compatible format +5. **`/.well-known/openid-configuration`** — OIDC discovery endpoint so Authentik can auto-configure itself +6. **`/health`** — health check endpoint used by Docker + +Tokens and auth codes are stored in memory and expire after 10 minutes. The bridge is stateless beyond that — no database required. + +## Requirements + +- A running Discourse instance with **DiscourseConnect provider** enabled (`/admin/site_settings` → search `sso provider`) +- A running [Authentik](https://goauthentik.io/) instance +- Docker (recommended) or Node.js 20+ + +## Setup + +### 1. Configure Discourse + +In your Discourse admin settings: + +- Enable `enable sso provider` +- Set `sso secret` — this becomes your `DISCOURSE_SECRET` +- Enable `verbose sso logging` while testing (optional but helpful) + +### 2. Configure the bridge + +Copy `.env.example` to `.env` and fill in your values: + +```bash +cp .env.example .env +nano .env +``` + +```env +# Port the bridge listens on inside Docker +PORT=3000 + +# Your Discourse instance +DISCOURSE_URL=https://discourse.example.com +DISCOURSE_SECRET=CHANGE_ME + +# The URL this bridge is accessible at (used in redirects) +BRIDGE_URL=https://auth-bridge.example.com + +# OAuth2 credentials — you will set these in Authentik (step 3) +OAUTH2_CLIENT_ID=CHANGE_ME +OAUTH2_CLIENT_SECRET=CHANGE_ME +``` + +Generate a strong secret for `OAUTH2_CLIENT_SECRET`: + +```bash +openssl rand -hex 32 +``` + +### 3. Configure Authentik + +In Authentik, create a new **Generic OAuth2 / OIDC provider**: + +| Setting | Value | +|---|---| +| Authorization URL | `https://auth-bridge.example.com/authorize` | +| Token URL | `https://auth-bridge.example.com/token` | +| Userinfo URL | `https://auth-bridge.example.com/userinfo` | +| Client ID | your chosen `OAUTH2_CLIENT_ID` | +| Client Secret | your chosen `OAUTH2_CLIENT_SECRET` | + +Or use the OIDC discovery URL and let Authentik configure itself: + +``` +https://auth-bridge.example.com/.well-known/openid-configuration +``` + +### 4. Run with Docker + +```bash +docker build -t discourse-oauth2-bridge . +docker run -d \ + --name discourse-oauth2-bridge \ + --env-file .env \ + -p 3000:3000 \ + discourse-oauth2-bridge +``` + +Or with Docker Compose — see the example below. + +### 5. Verify + +```bash +curl https://auth-bridge.example.com/health +# → {"status":"ok"} + +curl https://auth-bridge.example.com/.well-known/openid-configuration +# → OIDC discovery document +``` + +## Docker Compose example + +This is the configuration used on [tobiaseigen.org](https://tobiaseigen.org), where the bridge runs alongside Authentik on a mailcow server, sharing the mailcow Docker network: + +```yaml +services: + discourse-bridge: + build: . + container_name: authentik-discourse-bridge-1 + restart: unless-stopped + env_file: .env + networks: + mailcow-network: + ipv4_address: 172.22.1.247 # adjust to a free IP on your network + +networks: + mailcow-network: + external: true + name: mailcowdockerized_mailcow-network +``` + +## User info claims + +The `/userinfo` endpoint returns the following claims: + +| Claim | Source | +|---|---| +| `sub` | Discourse `external_id` | +| `preferred_username` | Discourse username | +| `name` | Discourse full name | +| `email` | Discourse email | +| `picture` | Discourse avatar URL | +| `groups` | Discourse group memberships (comma-separated list) | +| `discourse_admin` | `true` if Discourse admin | +| `discourse_moderator` | `true` if Discourse moderator | + +Authentik can use `groups` claims to map Discourse group membership to Authentik groups, which downstream apps can then use for access control. + +## Security notes + +- Auth codes, access tokens and pending auth state are stored **in memory only** — they do not survive a container restart +- All tokens expire after **10 minutes** +- The bridge validates Discourse's HMAC-SHA256 signature on every callback before trusting any user data +- The container runs as a non-root user (`bridge`) +- Keep your `.env` file private — it contains secrets. It is excluded from this repo via `.gitignore` + +## Background + +This bridge was developed for the [Digitally Sovereign](https://discourse.tobiaseigen.org) self-hosting project, where Discourse is used as the single source of truth for user identity and access control across a suite of self-hosted tools. See the forum for background and discussion. + +## License + +MIT