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.
  • JavaScript 96.5%
  • Dockerfile 3.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-10 02:05:20 +00:00
.env-example Improve .env-example with section headers and clearer comments 2026-08-21 19:08:32 +00:00
.gitignore Initial commit: discourse-oauth2-bridge 2026-06-23 21:13:40 +00:00
docker-compose.yml Add OIDC compliance: jwks endpoint, id_token, standalone compose 2026-10-09 23:22:48 +00:00
Dockerfile Initial commit: discourse-oauth2-bridge 2026-06-23 21:13:40 +00:00
LICENSE Add GPL v3 license 2026-08-21 19:02:57 +00:00
package-lock.json Add OIDC compliance: jwks endpoint, id_token, standalone compose 2026-10-09 23:22:48 +00:00
package.json Add OIDC compliance: jwks endpoint, id_token, standalone compose 2026-10-09 23:22:48 +00:00
README.md Update README.md 2026-10-10 02:05:20 +00:00
server.js Add OIDC compliance: jwks endpoint, id_token, standalone compose 2026-10-09 23:22:48 +00:00

discourse-oauth2-bridge

A lightweight Node.js bridge that allows Discourse to act as an OpenID Connect (OIDC) identity provider, enabling single sign-on for apps like Nextcloud, Immich, and others.

Discourse natively supports DiscourseConnect as an SSO provider, but most modern apps expect OIDC. This bridge translates between the two.

How it works

User → App (e.g. Nextcloud) → auth-bridge (OIDC) → Discourse (DiscourseConnect) → back to App

The bridge exposes a standards-compliant OIDC provider interface (discovery, authorization, token, userinfo, and JWKS endpoints) and handles the DiscourseConnect handshake with Discourse behind the scenes.

Claims exposed

Claim Source
sub Discourse external_id
preferred_username Discourse username
name Discourse full name
email Discourse email
groups Discourse group memberships
picture Discourse avatar URL

Prerequisites

  • Discourse with DiscourseConnect provider enabled (Admin → Settings → enable discourse connect provider)
  • Docker and Docker Compose
  • A subdomain for the bridge (e.g. auth-bridge.yourdomain.org) with SSL

Deployment

1. Clone the repo

git clone https://git.digitallysovereign.org/tobias/discourse-oauth2-bridge /opt/discourse-auth-bridge
cd /opt/discourse-auth-bridge

2. Configure environment

cp .env.example .env
nano .env
DISCOURSE_URL=https://discourse.yourdomain.org
DISCOURSE_SECRET=your_shared_secret
BRIDGE_URL=https://auth-bridge.yourdomain.org
OAUTH2_CLIENT_ID=your_client_id
OAUTH2_CLIENT_SECRET=your_client_secret
PORT=3000
Variable Description
DISCOURSE_URL Your Discourse instance URL
DISCOURSE_SECRET Shared secret — must match discourse connect provider secrets in Discourse admin
BRIDGE_URL Public URL of this bridge
OAUTH2_CLIENT_ID Client ID your downstream app uses to connect to this bridge
OAUTH2_CLIENT_SECRET Client secret your downstream app uses to connect to this bridge

3. Configure Discourse

In Discourse Admin → Settings:

  • Enable enable discourse connect provider
  • Under discourse connect provider secrets, add a row:
    • Secret: (your DISCOURSE_SECRET)
    • Allowed return URLs: https://auth-bridge.yourdomain.org/callback

4. Configure nginx

server {
    listen 80;
    server_name auth-bridge.yourdomain.org;
    return 301 https://$host$request_uri;
}
server {
    listen 443 ssl http2;
    server_name auth-bridge.yourdomain.org;
    ssl_certificate /etc/letsencrypt/live/auth-bridge.yourdomain.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/auth-bridge.yourdomain.org/privkey.pem;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

5. Start the bridge

docker compose up -d

Verify:

curl https://auth-bridge.yourdomain.org/.well-known/openid-configuration
curl https://auth-bridge.yourdomain.org/jwks

6. Create a systemd service

cat > /etc/systemd/system/discourse-bridge.service << 'EOF'
[Unit]
Description=Discourse OAuth2 Bridge
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/discourse-auth-bridge
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable discourse-bridge

Configuring Nextcloud user_oidc

Install the user_oidc app from the Nextcloud app store, then go to Admin Settings → OpenID Connect and register a new provider:

Field Value
Identifier Discourse
Client ID (your OAUTH2_CLIENT_ID)
Client Secret (your OAUTH2_CLIENT_SECRET)
Discovery endpoint https://auth-bridge.yourdomain.org/.well-known/openid-configuration
Scope openid profile email
Extra claims groups

Attribute mapping:

Field Value
User ID mapping preferred_username
Display name mapping name
Email mapping email
Groups mapping groups

Access control settings:

  • ☑️ Turn off hashed user ID (use plain text so Discourse username = Nextcloud username)
  • ☑️ Enable groups syncing
  • ☑️ Enable Allow API/WebDAV with OIDC token (required for desktop and mobile clients)
  • ☑️ Enable Fetch from userinfo endpoint

To make Nextcloud redirect automatically to Discourse on login:

php occ config:app:set user_oidc allow_multiple_user_backends --value=0

⚠️ This disables local password login. Make sure you have an emergency admin access method before enabling.

Troubleshooting

Symptom Cause Fix
Missing fields: jwks_uri in Nextcloud Old bridge version without JWKS support Update to latest version
Authentication failed due to missing secret DISCOURSE_SECRET mismatch Check Discourse admin settings match .env exactly; restart bridge after .env changes with docker compose up -d --force-recreate
Nextcloud username is not Discourse username User ID mapping wrong Set User ID mapping to preferred_username in user_oidc provider settings
Admin group lost after login No admin group in Discourse Create an admin group in Discourse and add yourself to it
Immich OAuth login fails after Nextcloud username rename Stale oauthId in Immich database Update oauthId in Immich's user table to match new username

Notes

  • The RSA key pair used to sign ID tokens is generated fresh on each container restart. Since tokens are short-lived (1 hour) this is fine in practice.
  • The bridge supports a single OIDC client. If you need multiple downstream apps, each should connect via Nextcloud's own OIDC Identity Provider app, using this bridge only for the Discourse → Nextcloud leg.

Architecture with Nextcloud as hub

Discourse (DiscourseConnect provider)
    │
    ▼
auth-bridge (this repo — translates to OIDC)
    │
    ▼
Nextcloud (user_oidc consumer + OIDC Identity Provider)
    │
    ├──▶ Immich
    ├──▶ Forgejo
    └──▶ (any other OIDC app)