Official Single-Sign-On for Discourse (sso)

Discourse now ships with official hooks to perform auth offsite.

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 SSO is to replace Discourse authentication, if you would like to add a new provider see existing plugins such as: Vk.com login (vkontakte)

Enabling SSO

To enable single sign on you have 3 settings you need to fill out:

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

Once enable_sso is set to true:

  • Clicking on login or avatar will, redirect you to /session/sso which in turn will redirect users to sso_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?

If you check enable_sso by mistake and need to revert to the original state and no longer have access to the admin panel, visit /users/admin-login and follow the instructions.

OR, from server console run:

cd /var/discourse
./launcher enter app
rails c
irb > SiteSetting.enable_sso = false
irb > SiteSetting.enable_local_logins = true
irb > exit
exit

Implementing SSO 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 sso_url with a signed payload: (say sso_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. The payload is always a valid querystring.

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

nonce=ABCD, this raw payload is base 64 encoded.

The endpoint being called must

  1. Validate the signature: ensure that HMAC-SHA256 of sso_secret, PAYLOAD is equal to the sig (sig will be hex encoded).
  2. Perform whatever authentication it has to
  3. Create a new 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.sso_overrides_username is set.
    • name will become the full name on Discourse if the user is new or SiteSetting.sso_overrides_name is set.
    • avatar_url will be downloaded and set as the user’s avatar if the user is new or SiteSetting.sso_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.sso_overrides_bio is set.
    • Additional boolean (“true” or “false”) fields are: admin, moderator, suppress_welcome_message
  4. Base64 encode the payload
  5. Calculate a HMAC-SHA256 hash of the payload using sso_secret as the key and Base64 encoded payload as text
  6. Redirect back to 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.

Specifying group membership

If the sso 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 sso 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:

A trivial implementation would be:

class DiscourseSsoController < ApplicationController
  def sso
    secret = "MY_SECRET_STRING"
    sso = 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.

The system always trusts emails provided by the single sign on endpoint. This means that if you had an existing account in the past on Discourse with SSO disabled, SSO will simply re-use it and avoid creating a new account.

If you ever turn off SSO, 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
SSO url : http://www.example.com/discourse/sso
SSO 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=\n

  • Payload is URL encoded: bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGI%3D%0A

  • HMAC-SHA256 is generated on the Base64 encoded Payload: 2828aa29899722b35a2f191d34ef9b3ce695e0e6eeec47deb46d588d70c7cb56

Finally browser is redirected to:

http://www.example.com/discourse/sso?sso=bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGI%3D%0A&sig=2828aa29899722b35a2f191d34ef9b3ce695e0e6eeec47deb46d588d70c7cb56

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 SSO records

You can use the POST admin endpoint /admin/users/sync_sso to synchronize an SSO record, pass it the same record you would pass to the SSO 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 as url parameters

Clearing SSO records

If your external_id values from your SSO 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 sso 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.sso_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

Debugging your SSO provider

To assist in debugging SSO you may enable the site setting verbose_sso_logging. By enabling that site setting rich diagnostics will show up in YOURSITE.com/logs

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

  • Every time the SSO process is initiated we will log a warning to the log with a full dump of the SSO payload

  • Every time a user fails to complete SSO (due to nonce expiring or ip block)

121 Likes
Discourse SSO + normal login
SSO login & logout issues
Is there a "log_in" SSO API endpoint?
Do you recommend that I host my Discourse and Wordpress site separately?
SSO locked me out of Discourse!
About the sso category
Login to Discourse with website account details
What is the SSO login URL
Automatically assigning users to a group
With SSO my user still need to hit the login button
Switching out authentication for a passwordless alternative
SSO integration & external profile sync help
SSO and e-mail addresses having a plus sign
Shibboleth / SAML / SSO -- Working Implementation for Higher Ed
Users who register on my site, register also on Discourse Vise Versa
Merging users from different forums
Sync SSO user data with the sync_sso route
Using existing RoR application for user auth / signup instead of discourse
How to handle Discourse SSO when the authentication site allows users to change emails?
Advantage and disadvantage of enabling SSO
SSO locked me out of Discourse!
SSO synced login state tips
Discourse view file update does not reflect in browser
Discourse view file update does not reflect in browser
Trying to set up SSO
Discourse doesn't re-verify an address changed by SSO
Discourse doesn't re-verify an address changed by SSO
Discourse SSO using auth0 via URL
Custom Login / Registration from another API
SSO and Discourse Consulting
Changing the unique key to identify users
Setting the user title(group?) based on the information that is coming from the sso payload
Mini (Inline) Onebox Support RFC
How to generate nonce from client-side Javascript
Advice needed for tailoring Discourse to my organisation
Recommended process for disabling SSO (Single-Sign-On)
Primary and Discourse Site Integrations
Categorizing and tracking users
SSO with TownNews CMS
Automatic Table of Contents generation
Questions Regarding Account Authentication Methods
Unified Experience / Custom Layouts - Recommended Approach
Redirect all users who click on domain.com/signup to a different page
Poll Result Breakdown
Cant update email via API - invalid_access error
Single Sign-Out?
Disabling all emails except those registration related?
SSO on Discourse using Atlassian Crowd
Can't get avatar overrides to work over SSO
[PAID] Setup SSO for self-hosted instance
Enable sso for my site
How to create and configure Custom User Fields
Which Discourse hosting tier should I choose?
New users via API if allow new unchecked
Discourse Ruby API testing "Unknown attribute 'auth_token' for User
How to change login settings without being logged in?
OpenId Connect Plugin with AWS Cognito
Create user in discourse by redirecting from another site
HMAC-256 example on Official SSO page
Add links to meta.discourse.org instructions inside admin
How to configure the SSO Authorization URL via config (without using the admin panel)
How do I connect dashboard with Discourse?
TeamAndro exploring a migration from phpBB
"User Log out API" return success in response but user session still alive
Dev Category sidebar
User Fields to validate users
Disabling SSO in development environment
Getting signed data from the server
Getting signed data from the server
Getting signed data from the server
SSO and changing email addresses upstream
SSO (maybe) specific issue
SSO (maybe) specific issue
Importing an IPB 3.1 Forum into Discourse
Integration with .NET MVC application for a SaaS platform
Running my own discourse image
A way for admins to edit users' external IDs
Automatic addition of users to group based on email domain
Allowing people to login using accounts from other websites
Seeking Slack Login / SSO for Discourse
Smooth J/K navigation when using keyboard
SSO to Joomla site
Implementing SSO for dev environment and troubleshoot
SSO provider implementation - Admin, moderator and groups ignored?
Will Discourse ask for a username if it's not provided to /session/sso_login?
How to divide my community into 2 parts
Using Discourse to add a forum feature to our current application?
Shared cookie SSO: Notifying frontend
User auth with website at root and Discourse in subfolder
SingleSignOnRecord.destroy_all - Imported users not usable (User {:primary_email=>"has already been taken"})
Can I authenticate to Drupal via Discourse?
Difficulty of Tiered-access forum
SSO Login page not showing up
Disable email verification for SSO
[PAID] automatically change user email
Is there a way to get all emails of users with the API?
Show/hide forums based on the domain? (Shared forum via CNAME)
Integrate with DjangoRest and Vue.js
Connect Discourse Auth with my Django user DB?
Disable account confirm emails when creating users via API
Transforming usernames with SSO
Automatically provision accounts with external SSO provider? (skip Create New Account prompt)
Embed variables in footer
How to detect Discourse user on Ghost Blog?
Is Roblox Login Possible?
Auto assign member to the certain group
Connecting to an external source of avatars?
Changing avatar_url while sso_overrides_avatar is set?
Disabling email verification
SSO Isnt working for me
Could Discourse offer a StackExchange-like SSO/Federated login service?
Intergrading discoures in to a application
How to connect to an external database running on localhost
Automate User Creation
Need help to setup SSO without emails
Login to Discourse with custom Oauth2 provider
Can I log into multiple instances of discourse simultaneously?
Upgraded last night and login button no longer works
SSO with Roles translating to Groups
Redirect login possible?
How to disable SSO via SSH
Connect Multiple WP Sites To 1 Discourse Installation?
What is the procedure to obtain CAS between my website and my discourse instance?
Problem logging in using SSO plugin
Can I remove the requirement for E-Mail signup?
PAID: Create Open Source SSO plugin to auth with Wild Apricot
Trouble connecting drupal and discourse
About the idea: IDENTITY = EMAIL
About the idea: IDENTITY = EMAIL
Consequences of not validating email addresses
SSO and Restricted Groups
Questions about Discourse on Digital Ocean
Require users to join at least one group at sign-up
Change right gutter to vertical timeline + topic controls
Use the same user database and login credentials in multiple discourse instances
How to connect my (existing) User Database?
SingleSignOnRecord.destroy_all - Imported users not usable (User {:primary_email=>"has already been taken"})
Options with SSO with another custom application
Issues in Integrating SSO in Discourse
Customized login auth plugin
How to implement Discourse with an already built Rails project
Updating SSO documentation
Configuring SSO to Work With SocialEngine
Updating SSO documentation
SSO synced login state tips