Official Single-Sign-On for Discourse (sso)

sso

(Sam Saffron) #1

Discourse now ships with official hooks to perform auth offsite.

The Problem

Many sites wish to integrate with a Discourse site, however want to keep all user registration in a separate site. In such a setup all Login operations should be outsourced to a 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
  2. Perform whatever authentication it has to
  3. Create a new payload with nonce, email, external_id and optionally (username, name)
  • 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
  1. Base64 encode the payload
  2. Calculate a HMAC-SHA256 hash of the payload using sso_secret as the key and Base64 encoded payload as text
  3. 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

You may specify group membership in your SSO payload using the add_groups and remove_groups attributes.

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

bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT1zYW0mdXNlcm5hbWU9\nc2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRlcm5hbF9pZD1oZWxsbzEyMyZyZXF1aXJl\nX2FjdGl2YXRpb249dHJ1ZQ==\n

  • Payload is URL encoded

bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT1zYW0mdXNlcm5hbWU9%0Ac2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRlcm5hbF9pZD1oZWxsbzEyMyZyZXF1aXJl%0AX2FjdGl2YXRpb249dHJ1ZQ%3D%3D%0A

  • Base64 encoded Payload is signed

1c884222282f3feacd76802a9dd94e8bc8deba5d619b292bed75d63eb3152c0b TODO update example - this is not correct signature

  • Browser redirects to:

http://discuss.example.com/session/sso_login?sso=bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT1zYW0mdXNlcm5hbWU9%0Ac2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRlcm5hbF9pZD1oZWxsbzEyMyZyZXF1aXJl%0AX2FjdGl2YXRpb249dHJ1ZQ%3D%3D%0A&sig=1c884222282f3feacd76802a9dd94e8bc8deba5d619b292bed75d63eb3152c0b

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.

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.

  • Consider adding a discourse_sso gem to make it easier to implement in Ruby.

Advanced Features

  • You can pass through custom user field now: Custom user fields for plugins
  • 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)

Updates:

2-Feb-2014

  • use HMAC-SHA256 instead of SHA256. This is more secure and cleanly separates key from payload.
  • removed return_url, the system will automatically redirect users back to the page they were on after login

4-April-2014

  • Added example

24-April-2014

  • Make note of custom user fields.

01-August-2014

  • Changed Rails console instructions to assume Docker setup

22-August-2014

  • Added option to override user avatar.

23-Oct-2014

  • Added end point for synchronizing SSO

4-Apr-2015

  • Added documentation about verbose sso logging

14-Apr-2016

  • Documented all accepted parameters under step 3 of “Implementing SSO on your site”

11-Nov-2016

  • Added note on add_groups and remove_groups

11-Oct-2017

  • Removed session expiry note from future work. This was implemented in July of 2016.

29-Nov-2017

  • add command to clear SSO records


Single-Sign-On for Discourse: groups
WordPress SSO Page Template
SSO user created with 1 appended at end of username
Pull user and password for custom app auth
Discourse as a CAS Server
SSO for dummies
Making external API call after logging in
Discourse as SSO source of authority for Wordpress
SSO with SAML (mod_shibboleth) and Flask
SSO login from main site backend
Do you recommend that I host my Discourse and Wordpress site separately?
Discourse API - Login case SSO Provider to Consumer site
SSO locked me out of Discourse!
How to implement delegated SSO?
Using an existing mysql DB to auth discourse users
What are the rules for usernames?
User is not logged in after SSO sequence
WP Discourse SSO Plugin
Discourse SSO + normal login
Users who register on my site, register also on Discourse Vise Versa
ASP.NET MVC Single-Sign-On
About the sso category
Discourse single-sign-on integration with SharePoint
SSO locked me out of Discourse!
SSO does not redirect, is canceled instead
Smoothly integrating Discourse with an existing social site
Help with Okta SSO
Merging users from different forums
Is there a "log_in" SSO API endpoint?
Using existing RoR application for user auth / signup instead of discourse
SSO Client avatars
Discourse as Rails engine
Using Stripe for Members only Group and Category
How to control which email messages send and do not send
SSO on Discourse using Atlassian Crowd
Login to Discourse with custom Oauth2 provider
Advantage and disadvantage of enabling SSO
Syncing the editor viewport scroll
ASP.NET MVC Single-Sign-On
Syncing the editor viewport scroll
Can I log into multiple instances of discourse simultaneously?
Upgraded last night and login button no longer works
Can't get avatar overrides to work over SSO
RuntimeError Bad signature for payload during SSO login and signup
Disable SSO While Not Signed In
Python 3 - SSO Helper Class
Unified Experience / Custom Layouts - Recommended Approach
Discourse integration with Grails
Login to Discourse with website account details
SSO for dummies
Single Sign-Out?
How do I connect dashboard with Discourse?
SSO and changing email addresses upstream
Wp-discuss integration error
"User Log out API" return success in response but user session still alive
How to disable SSO via SSH
Discourse SSO Logout
Connect Multiple WP Sites To 1 Discourse Installation?
Link "sign up" and "create an account" buttons to different URL
Wrong-direction 'linked post' notification
SSO (maybe) specific issue
How to use the Discourse auth system for another app?
What is the procedure to obtain CAS between my website and my discourse instance?
HMAC-256 example on Official SSO page
Problem logging in using SSO plugin
SSO (maybe) specific issue
Importing an IPB 3.1 Forum into Discourse
Can I remove the requirement for E-Mail signup?
Topics and replies not attributed to correct user
Usernames with periods are changed to underscore
PAID: Create Open Source SSO plugin to auth with Wild Apricot
Integration with .NET MVC application for a SaaS platform
Trouble connecting drupal and discourse
About the idea: IDENTITY = EMAIL
About the idea: IDENTITY = EMAIL
OAuth2 Basic Support
How to avoid "Account login timed out, please try logging in again" when the payload had expired in SSO
Any way to bulk load/create SSO users?
SSO with SAML (mod_shibboleth) and Flask
SSO with SAML (mod_shibboleth) and Flask
User Fields to validate users
How to change sso_url from console
OAuth2 Basic Support
Consequences of not validating email addresses
Transition from Listserv (lsoft) to Discourse
SSO and Restricted Groups
Questions about Discourse on Digital Ocean
SSO and e-mail addresses having a plus sign
I need ideas for a migration strategy from dual logins and phpBB to new SSO and Discourse
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?
Use main website as Oauth Server for my discourse forum
SSO : what is the use of the about_me attribute
SSO login & logout issues
Getting signed data from the server
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
Configuring SSO to Work With SocialEngine
SSO synced login state tips
SSO synced login state tips
Discourse view file update does not reflect in browser
Discourse view file update does not reflect in browser
Discourse API - where to start? what is my API url?
Trying to set up SSO
Discourse API - where to start? what is my API url?
Discourse doesn't re-verify an address changed by SSO
Discourse doesn't re-verify an address changed by SSO
SAML plugin in repo. Multisite
SSO integration & external profile sync help
Shibboleth / SAML / SSO -- Working Implementation for Higher Ed
SSO and Discourse Consulting
Mini (Inline) Onebox Support RFC
With SSO my user still need to hit the login button
Looking for help with single sign on with PHP server
Creating discourse account without email
How to generate nonce from client-side Javascript
Enabled SSO and now I can't login to site anymore
Advice needed for tailoring Discourse to my organisation
Getting signed data from the server
Getting signed data from the server
Changing the unique key to identify users
Sync SSO user data with the sync_sso route
Add links to meta.discourse.org instructions inside admin
What is the SSO login URL
Redirect all users who click on domain.com/signup to a different page
Questions Regarding Account Authentication Methods
How to configure the SSO Authorization URL via config (without using the admin panel)
Sign up and local authentication disappeared after enabling SSO-based authentication
Disabling all emails except those registration related?
How to change login settings without being logged in?
Discourse Ruby API testing "Unknown attribute 'auth_token' for User
Updating SSO documentation
New users via API if allow new unchecked
(Sam Saffron) #102

(Michael) #198

What’s the logic behind disabling Discourse’s built-in authentication when SSO is enabled? I’m sure there’s a reason, I’m just having trouble reasoning about it.

For context, here’s what my group would like to do: we have our app running with its own authentication. Our Discourse instance is running elsewhere. We’d like to have users be able to view and participate in the forum without necessarily having an account with our app, but with the possibility that they will eventually have an app account (and then use the app login to access Discourse via the SSO method).


(Kane York) #199

You need to build it as an OAuth provider, then. The idea with SSO is that it’s the single source of authority - for example, people hooking it up to their Active Directory domains. That clearly isn’t what you want, so…

What you can do right now is enable Google/Facebook/Yahoo login (these are all ~3 clicks), then add a “user custom field” for their account name on your service.


(dmchicago) #223

Is there any way to assign a group from the external site when using SSO? Here is the use case:

We have multiple tiers of users on our site. When they go to discourse forum and login via SSO, we would like to lookup their membership tier, and assign them to specific discourse group so that they can participate in “premium members only” discussion category. Is that possible?


#252

I’m wondering how does the SSO code handle this case:

  1. User creates account on sso.example.tld
  2. User signs on to forum.example.tld
  3. User goes to profile on sso.example.tld and changes their email address
  4. User then goes to forum.example.tld

At step 4, will Discourse still be able to figure out that the User from sso.example.tld with a changed email address is the same user in Discourse’s User database that has the user’s old email address stored?
If it can’t and instead creates a new Discourse user for that person because of the changed email address then that would be confusing and problematic for the user and the admin.


(Becky Herndon) #253

I just went through this same research. After the account is created, every time User’s payload is sent to discourse this will happen.

The User is logged into the forum account associated with your external ID, and it will change the email address to User’s new address.


(Felix Freiberger) #254

Note that this only happens if sso overrides email is on – otherwise, the address submitted via SSO is only used for the first lookup or when the account is created.


(Jeremy) #260

Is there any way to automatically sign them in when they hit the discourse forums, if they are already logged into our system? If do they always have to hit “Login” on the discourse forum first?


(Felix Freiberger) #261

If your forum should only be available to SSO users, you can enable the require login site setting, and users will be logged in automatically. If not: I don’t think there is an easy way to do it.


(Dylan Damsma) #262

What I’ve done is sending users that are logged in on our platform to the link:
discuss.example.com/login

and those that are not logged in to:
discuss.example.com

Not sure if it’s the right method, but seems to work for us.


(Jesse Perry) #263

Is there a way to disable #3 in this list and make the login fail if the user doesn’t already exist (if external_id or matching email doesn’t match) on the Discourse?


(Ionut Georgian Ciobanu De Radu) #265

Two questions:

  1. Why is there a \n added to the end of the the base6d encoded string?
  2. Could you please provide a complete payload and signature example to send to Discourse?

1c884222282f3feacd76802a9dd94e8bc8deba5d619b292bed75d63eb3152c0b TODO update example - this is not correct signature


(Brett Wallace) #266

Quick question if anybody has a minute to answer.

If a user is inactivated on the site used for SSO, how does discourse know/handle it?


(Felix Freiberger) #267

It doesn’t – it’s the SSO site’s responsibility to handle this. It should prevent new sign-ins and possibly log the user out remotely via the API*.

*It would be awesome if Discourse had an API to log out users using their external_id


(Kane York) #268

Yeah, right now that takes two requests - first is /users/by_external and second is the log out.


(Guss Davey) #269

What if I want to do it a different way round. I want Discourse to register the user, but then I want to pull the data to my home site, for the additional non forum features I provide my customers?

OR authenticate my websites login again Discourse (being the end point)


(Daniel Lynch) #270

@Guss I think what you want is Using Discourse as a SSO provider


(Sam Saffron) #282

Note, I just added optional rich logging for SSO via:

This site setting verbose_sso_logging can help you debug your SSO provider, however, be careful it gets very loud… only enable while you are debugging issues with your provider.


(Blake Erickson) #372

I created a very simple but working example SSO Endpoint that anyone can use for testing or learning how SSO works:


Bratwurst: the wurst possible production SSO provider