Setup DiscourseConnect - Official Single-Sign-On for Discourse (sso)

DiscourseConnect is a core Discourse feature that allows you to configure “Single Sign-On (SSO)” to completely outsource all user registration and login from Discourse to another site. Offered to our standard, business and enterprise hosting customers.

:information_source: (Feb 2021) ‘Discourse SSO’ is now ‘DiscourseConnect’. If you are running an old version of Discourse, the settings below will be named sso_... rather than discourse_connect_...

The Problem

Many sites wishing to integrate with a Discourse site want to keep all user registration in a separate site. In such a setup all login operations should be outsourced to that different site.

What if I would like SSO in conjunction with existing auth?

The intention around DiscourseConnect is to replace Discourse authentication, if you would like to add a new provider see existing plugins such as: Discourse VK Authentication (vkontakte)

Enabling DiscourseConnect

To enable DiscourseConnect you have 3 settings you need to fill out:

enable_discourse_connect : must be enabled, global switch
discourse_connect_url: the offsite URL users will be sent to when attempting to log on
discourse_connect_secret: a secret string used to hash SSO payloads. Ensures payloads are authentic.

Once enable_discourse_connect is set to true:

  • Clicking on login or avatar will, redirect you to /session/sso which in turn will redirect users to discourse_connect_url with a signed payload.
  • Users will not be allowed to “change password”. That field is removed from the user profile.
  • Users will no longer be able to use Discourse auth (username/password, google, etc)

What if you check it by mistake?

See: Log back in as admin after locking yourself out with read-only mode or an invalid SSO configuration

Implementing DiscourseConnect on your site

:warning: Discourse uses emails to map external users to Discourse users, and assumes that external emails are secure. IF YOU DO NOT VALIDATE EMAIL ADDRESSES BEFORE SENDING THEM TO DISCOURSE, YOUR SITE WILL BE EXTREMELY VULNERABLE!

Alternatively, if you insist on sending unvalidated emails BE SURE to set require_activation=true, this will force all emails to be validated by Discourse. WE STILL STRONGLY ADVISE THAT YOU DO NOT DO THIS, so if you proceed with that setting enabled, you are assuming substantial risk.

Discourse will redirect clients to discourse_connect_url with a signed payload: (say discourse_connect_url is https://somesite.com/sso)

You will receive incoming traffic with the following

https://somesite.com/sso?sso=PAYLOAD&sig=SIG

The payload is a Base64 encoded string comprising of a nonce, and a return_sso_url. The payload is always a valid querystring.

For example, if the nonce is ABCD. raw_payload will be:

nonce=ABCD&return_sso_url=https%3A%2F%2Fdiscourse_site%2Fsession%2Fsso_login, this raw payload is base 64 encoded.

The endpoint being called must

  1. Validate the signature: ensure that HMAC-SHA256 of PAYLOAD (using discourse_connect_secret, as the key) is equal to the sig (sig will be hex encoded).
  2. Perform whatever authentication it has to
  3. Create a new url-encoded payload with at least nonce, email, and external_id. You can also provide some additional data, here’s a list of all keys that Discourse will understand:
    • nonce should be copied from the input payload
    • email must be a verified email address. If the email address has not been verified, set require_activation to “true”.
    • external_id is any string unique to the user that will never change, even if their email, name, etc change. The suggested value is your database’s ‘id’ row number.
    • username will become the username on Discourse if the user is new or SiteSetting.auth_overrides_username is set.
    • name will become the full name on Discourse if the user is new or SiteSetting.auth_overrides_name is set.
    • avatar_url will be downloaded and set as the user’s avatar if the user is new or SiteSetting.discourse_connect_overrides_avatar is set.
    • avatar_force_update is a boolean field. If set to true, it will force Discourse to update the user’s avatar, whether avatar_url has changed or not.
    • bio will become the contents of the user’s bio if the user is new, their bio is empty or SiteSetting.discourse_connect_overrides_bio is set.
    • Additional boolean (“true” or “false”) fields are: admin, moderator, suppress_welcome_message
  4. Base64 encode payload
  5. Calculate a HMAC-SHA256 hash of the payload using discourse_connect_secret as the key and Base64 encoded payload as text
  6. Redirect back to the return_sso_url with an sso and sig query parameter (http://discourse_site/session/sso_login?sso=payload&sig=sig)

Discourse will validate that the nonce is valid, and if valid, it will expire it right away so it can not be used again. Then, it will attempt to:

  1. Log the user on by looking up an already associated external_id in the SingleSignOnRecord model
  2. Log the user on by using the email provided (updating external_id) (unless require_activation = true)
  3. Create a new account for the user providing (email, username, name) updating external_id

Security concerns

The nonce (one time token) will expire automatically after 10 minutes. This means that as soon as the user is redirected to your site they have 10 minutes to log in / create a new account.

The protocol is safe against replay attacks as nonce may only be used once. The nonce is tied to the current browser session to protect against CSRF attacks.

Specifying group membership

If the discourse connect overrides groups option is specified, Discourse will consider the comma separated list of groups passed in groups.

Aside from groups, you may also specify group membership in your SSO payload using the add_groups and remove_groups attributes regardless of the discourse connect overrides groups option.

add_groups is a comma delimited list of group names we will ensure the user is a member of.
remove_groups is a comma delimited list of group names we will ensure the user is not a member of.

Reference implementation

Discourse contains a reference implementation of the SSO class:

discourse/lib/discourse_connect_base.rb at main · discourse/discourse · GitHub

A trivial implementation would be:

class DiscourseSsoController < ApplicationController
  def sso
    secret = "MY_SECRET_STRING"
    sso = DiscourseApi::SingleSignOn.parse(request.query_string, secret)
    sso.email = "user@email.com"
    sso.name = "Bill Hicks"
    sso.username = "bill@hicks.com"
    sso.external_id = "123" # unique id for each user of your application
    sso.sso_secret = secret

    redirect_to sso.to_url("http://l.discourse/session/sso_login")
  end
end

Transitioning to and from single sign on.

As long as the require_activation parameter is not set to true in the request payload, the system will trusts emails provided by the single sign on endpoint. This means that if you had an existing account in the past on Discourse with DiscourseConnect disabled, DiscourseConnect will simply re-use it and avoid creating a new account.

If you ever turn off DiscourseConnect, users will be able to reset passwords and gain access back to their accounts.

Real world example:

Given the following settings:

Discourse domain: http://discuss.example.com
DiscourseConnect url : http://www.example.com/discourse/sso
DiscourseConnect secret: d836444a9e4084d5b224a60c208dce14
Email validated: No (add require_activation=true to the payload)

User attempt to login

  • Nonce is generated: cb68251eefb5211e58c00ff1395f0c0b

  • Raw payload is generated: nonce=cb68251eefb5211e58c00ff1395f0c0b

  • Payload is Base64 encoded: bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGI=

  • Payload is URL encoded: bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGI%3D

  • HMAC-SHA256 is generated on the Base64 encoded Payload: 1ce1494f94484b6f6a092be9b15ccc1cdafb1f8460a3838fbb0e0883c4390471

Finally browser is redirected to:

http://www.example.com/discourse/sso?sso=bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGI%3D&sig=1ce1494f94484b6f6a092be9b15ccc1cdafb1f8460a3838fbb0e0883c4390471

On the other end

  1. Payload is validated using HMAC-SHA256, if the sig mismatches, process aborts.
  2. By reversing the steps above nonce is extracted.

User logs in:

name: sam
external_id: hello123
email: test@test.com
username: samsam
require_activation: true

Unsigned payload is generated:

nonce=cb68251eefb5211e58c00ff1395f0c0b&name=sam&username=samsam&email=test%40test.com&external_id=hello123&require_activation=true

order does not matter, values are URL encoded

Payload is Base64 encoded

bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT1zYW0mdXNlcm5hbWU9c2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRlcm5hbF9pZD1oZWxsbzEyMyZyZXF1aXJlX2FjdGl2YXRpb249dHJ1ZQ==

Payload is URL encoded

bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT1zYW0mdXNlcm5hbWU9c2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRlcm5hbF9pZD1oZWxsbzEyMyZyZXF1aXJlX2FjdGl2YXRpb249dHJ1ZQ%3D%3D

Base64 encoded Payload is signed

3d7e5ac755a87ae3ccf90272644ed2207984db03cf020377c8b92ff51be3abc3

Browser redirects to:

http://discuss.example.com/session/sso_login?sso=bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT1zYW0mdXNlcm5hbWU9c2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRlcm5hbF9pZD1oZWxsbzEyMyZyZXF1aXJlX2FjdGl2YXRpb249dHJ1ZQ%3D%3D&sig=3d7e5ac755a87ae3ccf90272644ed2207984db03cf020377c8b92ff51be3abc3

Synchronizing DiscourseConnect records

You can use the POST admin endpoint /admin/users/sync_sso to synchronize a DiscourseConnect record, pass it the same record you would pass to the DiscourseConnect endpoint, nonce does not matter.

If you call admin/users/sync_sso from another site, you will need to include a valid admin api_key and a valid api_username in the request’s headers. See Sync DiscourseConnect user data with the sync_sso route for more details about how to structure the request.

Clearing DiscourseConnect records

If your external_id values from your DiscourseConnect provider have changed (perhaps you changed the generation algorithm, perhaps it’s a different endpoint) you can safely remove all the existing records using the rails console:

SingleSignOnRecord.destroy_all

Logging off users

You can use the POST admin endpoint /admin/users/{USER_ID}/log_out to log out any user in the system if needed.

To configure the endpoint Discourse redirects to on logout search for the logout redirect setting. If no URL has been set here you will be redirected back to the URL configured in discourse connect url.

Search users by external_id

User profile data can be accessed using the /users/by-external/{EXTERNAL_ID}.json endpoint. This will return a JSON payload that contains the user information, including the user_id which can be used with the log_out endpoint.

Existing implementations

  • The discourse_api gem can be used for SSO. Have a look at the SSO code in its examples directory to see a basic implementation.

  • Our WordPress plugin makes it easy to configure SSO between WordPress and Discourse. Details about setting it up are found on the SSO tab of the plugin’s options page.

Future work

  • We would like to gather more reference implementations for SSO on other platforms. If you have one please post to the Dev / SSO category.

Advanced Features

  • You can pass through custom user fields by prefixing the field name with custom. For example custom.user_field_1 can be used to set the value of the UserCustomField that has the name user_field_1.
  • You can pass avatar_url to override user avatar (SiteSetting.discourse_connect_overrides_avatar needs to be enabled). Avatars are cached so pass avatar_force_update=true to force them to update if the url is the same. Right now, you can’t pass an empty url to disable users’ avatar.
  • By default the welcome message will be sent to all new users created through SSO. If you wish to suppress this you can pass suppress_welcome_message=true
  • To configure your Discourse instance as a Discourse connect provider see: Using DiscourseConnect as an identity provider.

Debugging your DiscourseConnect provider

To assist in debugging DiscourseConnect you may enable the site setting verbose_discourse_connect_logging. By enabling that site setting rich diagnostics will show up in YOURSITE.com/logs. Be sure to :white_check_mark: the warnings box at the bottom of YOURSITE.com/logs.

We will log a warning to the logs with a full dump of the SSO payload:

Last edited by @sam 2024-08-27T04:19:09Z

Check documentPerform check on document:
173 Likes

Hello, It’s hard for me to find anywhere, but which protocol does this use? Assume oauth2? The parameters don’t seem to match, and i get an error with the provider that it seems to be accepting an sso= parameter? Help!

thanks

DiscourseConnect is Discourse’s implementation of SSO. It doesn’t use a standard protocol.

If you don’t mind looking at PHP code, there’s an example implementation here: wp-discourse/lib/sso-provider/discourse-sso.php at main · discourse/wp-discourse · GitHub.

Yeah, that’s not going to work. If you have an OAuth2 provider that you’d like to use for authenticating users, have a look at the Discourse OAuth2 Basic plugin.

1 Like

Thanks @simon. is the PHP code a provider as well, and not consumer? Also saw an OIDC provider as well that may work and also a “middle-man” provider in the plugins area.

If the SSO provider is non-standard, then who/what is it intended for if it doesnt play nice with others?

Thanks again!

The code that I linked to is for using WordPress as the authentication provider for Discourse.

The WordPress plugin also allows WordPress to be used as a DiscourseConnect client: wp-discourse/lib/sso-client at main · discourse/wp-discourse · GitHub.

I’m not sure what the motivation was for adding a custom SSO implementation to Discourse. I’m guessing there was a business case for it.

One benefit it provides is that it allows an external site to be tightly integrated with Discourse. For example, all the user attributes listed here can be synced with Discourse during the auth process: discourse/lib/discourse_connect_base.rb at 7b89fdead98606d4f47ceb0a1d240d0f6e5f589e · discourse/discourse · GitHub.

It also allows sites that aren’t configured to be OAuth2 or OpenID Connect providers to be used for authenticating users on Discourse.

The downside is that it requires adding some custom code to the auth provider site.

1 Like

Hi, I’m curious what the issues are with not verifying email addresses on an external site that provides SSO. Is it just that it enables automated spamming? Or are there other considerations? Why is it not recommended that Discourse handles email verification if the external site does not?

Thanks for any additional insight.

The worst case scenario I’m aware of requires these conditions:

  • email addresses are not verified on the external site
  • require_activation=true is not set in the SSO payload
  • there are existing accounts on the Discourse site that do not have a SingleSignOnRecord associated with them (the account owner has never logged into Discourse with SSO)

For that case, someone could signup on the external site using the email address of a Discourse user who has never signed in with SSO. This would allow the unverified account from the external site to take over the Discourse account that uses the same email address. This would be especially concerning if it was an admin account on Discourse.

It is recommended that Discourse handles email verification if the external site isn’t handling it:

There are a couple of reasons that it’s better to handle email verification on the external site though:

  • forcing users to receive the confirmation email from Discourse adds some friction when users first attempt to login to Discourse. (Realistically, that friction has to happen somewhere though - either on the Discourse end or on the external site’s end.)
  • Discourse won’t match existing Discourse accounts to external logins based on the email address if require_activation is set to true in the SSO payload. This is an issue if you enable DiscourseConnect after some accounts have been created on Discourse by registering with a username/password. It’s also an issue if for any reason you ever need to delete SingleSignOnRecord entries on Discourse. Discourse won’t automatically create new SingleSignOnRecord entries when users attempt to log back into Discourse.
4 Likes

Thanks, @simon - that’s very helpful!

Hello, I have a question about the groups field in the SSO payload.

Will automated groups (like admins, moderators and the trust levels) also be overwritten? Or will they be retained?

Nope! Assuming the description of that setting is right, it’ll only affect manual groups.

You know what… I didn’t see the word “manual” in the description. Sounds promising for my use case, I’ll give it a try and report back.

1 Like

When I read this I thought I was supposed to generate the signature directly from the base64-encoded payload. I didn’t realise you need to generate it from the UTF-8 bytes. Could this be clarified?

I’ve been playing with DiscourseConnect - the documentation is great, thanks.
However i’ve hit a few roadbumps i’m hoping for some help/clarification on.

We are wanting users to be able to login to Wordpress using their Discourse login (that is working fine :slight_smile: ) - however.

  • Is it possible to create a Wordpress User via Discourse Signup (when a user creates a Discourse user account, it automatically creates a wordpress account/profile for them so they can log into wordpress)

  • Is it possible to sync Workpress User Groups and Discourse Groups.
    If a user has both a Wordpress and Discourse user account, DiscourseConnect is able to join them - but doesn’t give the wordpress account the User Groups of the Discourse Groups of the same name, and vis versa (and what about if groups don’t have the same name - how can i tell DiscourseConnect to give the User Group ‘Testing Group’ when the user has the Discourse Group ‘Group for Testing Things’?

What am i missing?

I don’t know the answer, but…

Be cautious with that. It is somewhere unlegal and quite widely counted bad practise (outside site owners, of course :smirk:) because that would happen without user’s consent and knowing, and at same time would data moved to somewhere else.

Sure, that is somewhat on gray area and basically, for example, Google is doing that.

But… why? Limit login only to Discourse SSO on Wordpress side and redirect users to Discourse for account creation and that’s it. But AFAIK you can’t sync user accounts automatically out of the box. And why should you, because with SSO it happens when a user needs it.

In our scenario (as a membership organisation)

  • Wordpress is used to manage subscriptions, purchase items on the wordpress store, and we use User Groups to manage what a member can do in the organisation
  • Discourse is our forum/online-community and we use Groups to control what areas of Discourse a user has access to)

Currently a new member needs to setup a Wordpress Account (and setup thier subscription etc.) and also setup a Discourse account, and User Group-Discourse Groups are manually managed/synced.

I’m trying to find a solution where a new user does setup once to create both accounts, and User Group-Discourse Groups are synced automatically - i’m sure i can figure out the Group sync with apis etc. It’s the multi user account setup set i’m trying to resolve/prevent

It sounds like what you are doing is using Discourse as the SSO provider for WordPress. That approach is outlined here: Use Discourse as an identity provider (SSO, DiscourseConnect). The Discourse WordPress plugin has options for either using WordPress as the SSO provider for Discourse, or for using Discourse as an identity provider for WordPress. Using the same name for both approaches leads to some confusion.

I’d be tempted to use WordPress as the identity provider for this case. With that approach, users will create accounts on your WordPress site and then login to Discourse with their WordPress credentials. One thing to be aware of with this approach is that it means that users will only be able to log into Discourse through WordPress, it won’t be possible to create a Discourse account without already having a WordPress account. I think that’s the appropriate setup when integrating Discourse with a WordPress membership site.

When WordPress is used as the identity provider for Discourse, there are a couple of utility functions that are useful for setting user’s Discourse group memberships based on their activity on WordPress. Those functions are outlined here: Manage group membership in Discourse with WP Discourse SSO.

Going back to your original question:

It’s been a while since I looked at the WordPress plugin’s DiscourseConnect Client code, but I think what you are asking for is more or less the way that code is expected to work. If a user has a Discourse account, they just need to click the “Login through Discourse” link on WordPress and an account will be created for them.

This would be technically possible when using WordPress as the DiscourseConnect Client, but unless something has changed, you won’t be able to use the add_user_to_discourse_group and remove_user_from_discourse_group methods that are outlined in the documentation I linked to. You’d need to so something like setup a Discourse Webhook that was triggered when a user was added to a Discourse group, then add some code on WordPress to process that webhook. To sync groups from WordPress to Discourse, you’d need to make an API call to Discourse to update a user’s groups when there was a change on WordPress. So something that would be fairly easy to accomplish if you use Wordpress as the DiscourseConnect provider might be somewhat complicated if you use WordPress as the DiscourseConnect client.

1 Like

Except if custom login is used, as usually is the situation with WooCommerce/memberships/LLM showing that button and forcing using only Discourse SSO as a provider isn’t happening out of the box and needs some custom work.

There are a couple of possible problems, one related to caching and another related to login redirects that are added by some plugins. Anyone running into those issues should ask about them in the WordPress category. They’re usually easily solved.

I totally forgot to report back: Indeed it works exactly as described, only manual groups are affected.