# 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