Discourse + SSO: Implement Single Sign-On with OAuth2 and OIDC

Discourse + SSO: Implement Single Sign-On with OAuth2 and OIDC

Managing separate logins for your community forum is a headache nobody asked for. Your users already have accounts in your main app, your CRM, or your identity provider. Making them create yet another password for Discourse is friction you can eliminate.

Discourse ships with solid SSO support out of the box. You have three main options: OpenID Connect (bundled with core), OAuth2 Basic (plugin), and DiscourseConnect (Discourse's native protocol). Each has tradeoffs depending on your existing auth infrastructure.

OpenID Connect: The Modern Standard

If your identity provider supports OIDC (Keycloak, Auth0, Okta, Azure AD), this is the cleanest path. The plugin ships with Discourse core, so there's nothing to install.

Head to Admin → Settings → Login and configure these values:

openid_connect_enabled: true
openid_connect_discovery_document: https://your-idp.com/.well-known/openid-configuration
openid_connect_client_id: discourse-community
openid_connect_client_secret: your-secret-here
openid_connect_authorize_scope: openid profile email

The discovery document does the heavy lifting. Discourse automatically fetches your provider's endpoints for authorization, token exchange, and user info.

For Keycloak specifically, your discovery URL looks like:

https://keycloak.example.com/realms/your-realm/.well-known/openid-configuration

Set your redirect URI in your identity provider to:

https://forum.example.com/auth/oidc/callback

Restart Discourse and your OIDC login button appears on the login page.

OAuth2 Basic: For Non-Standard Providers

Some systems expose OAuth2 but skip the OIDC layer. The OAuth2 Basic plugin handles these cases when you have a JSON endpoint that returns user data.

Install the plugin by adding to your app.yml:

hooks:
  after_code:
    - exec:
        cd: $home/plugins
        cmd:
          - git clone https://github.com/discourse/discourse-oauth2-basic.git

Rebuild and configure:

oauth2_enabled: true
oauth2_client_id: your-client-id
oauth2_client_secret: your-secret
oauth2_authorize_url: https://your-app.com/oauth/authorize
oauth2_token_url: https://your-app.com/oauth/token
oauth2_user_json_url: https://your-app.com/api/user
oauth2_json_user_id_path: id
oauth2_json_username_path: username
oauth2_json_email_path: email

The _path settings map JSON fields from your user endpoint to Discourse fields. If your API returns nested data like {"user": {"email": "..."}}, use user.email as the path.

DiscourseConnect: Full Control

When you need complete control over the authentication flow, DiscourseConnect (formerly Discourse SSO) lets your application be the identity source. Discourse redirects users to your endpoint, you validate them, and send back signed user data.

Enable it in Admin → Settings → Login:

discourse_connect_url: https://your-app.com/discourse/sso
discourse_connect_secret: generate-a-strong-secret-here

Your endpoint receives a sso parameter (base64-encoded payload) and a sig (HMAC-SHA256 signature). Validate the signature, authenticate the user however you want, then redirect back with user data.

Here's a Node.js implementation:

const crypto = require('crypto');

app.get('/discourse/sso', (req, res) => {
  const { sso, sig } = req.query;
  const secret = process.env.DISCOURSE_SSO_SECRET;

  // Verify signature
  const expected = crypto.createHmac('sha256', secret)
    .update(sso).digest('hex');
  if (sig !== expected) return res.status(403).send('Invalid signature');

  // Decode payload and get nonce
  const payload = Buffer.from(sso, 'base64').toString();
  const nonce = new URLSearchParams(payload).get('nonce');

  // Build response with your authenticated user
  const user = req.session.user; // Your auth system
  const responsePayload = new URLSearchParams({
    nonce,
    email: user.email,
    external_id: user.id,
    username: user.username,
    name: user.fullName,
    admin: user.isAdmin ? 'true' : 'false'
  }).toString();

  const responseBase64 = Buffer.from(responsePayload).toString('base64');
  const responseSig = crypto.createHmac('sha256', secret)
    .update(responseBase64).digest('hex');

  res.redirect(`https://forum.example.com/session/sso_login?sso=${responseBase64}&sig=${responseSig}`);
});

DiscourseConnect also supports group sync. Add add_groups and remove_groups parameters to automatically manage forum groups based on your application's roles.

Syncing User Data

All three methods support ongoing sync. Enable these settings to keep user profiles current:

sso_overrides_email: true
sso_overrides_username: true
sso_overrides_name: true
sso_overrides_avatar: true

For group synchronization with OIDC, map your provider's groups claim:

openid_connect_claims_groups: groups

Troubleshooting

Login redirects but returns to login page: Check your redirect URI matches exactly what's configured in your identity provider. Trailing slashes matter.

User created but email not verified: Enable sso_overrides_email and ensure your provider includes email verification status.

Groups not syncing: Verify your identity provider includes the groups claim in the token. Use jwt.io to inspect your tokens during testing.

CORS errors on token exchange: This happens when the browser tries to call your token endpoint directly. Ensure your OAuth2 flow uses server-side token exchange, not implicit flow.

Avatar not updating: The avatar URL must be publicly accessible. Discourse fetches it server-side and caches aggressively. Clear the cache with rake avatars:refresh.

Session timeout mismatch: If users get logged out of Discourse while still logged into your main app, align the session durations. Set maximum_session_age in Discourse to match your identity provider's token lifetime.

Multiple SSO providers: Discourse supports enabling multiple authentication methods simultaneously. Users can link accounts later if they initially sign up with a different provider.

Security Considerations

Never store your SSO secret in version control. Use environment variables or a secrets manager. For DiscourseConnect, generate secrets with at least 32 characters of randomness:

openssl rand -hex 32

Enable discourse_connect_overrides_groups only if you trust your identity provider completely. This setting allows external systems to grant admin access to users.

Consider setting require_activation to false when using SSO. Your identity provider already verified the email, so double-verification adds friction without security benefit.

Deploy Discourse with SSO Ready

Setting up Discourse with proper SSO configuration takes careful planning. Deploy Discourse on Elestio and get a production-ready instance with automated backups and updates. Focus on configuring your auth flow instead of managing infrastructure.

Thanks for reading!