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.

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.
  • 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)
  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:

29-Nov-2017

  • add command to clear SSO records

14-Apr-2016

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

4-Apr-2015

  • Added documentation about verbose sso logging

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

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.


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
Do you recommend that I host my Discourse and Wordpress site separately?
SSO login from main site backend
SSO with SAML (mod_shibboleth) and Flask
Discourse as SSO source of authority for Wordpress
Using an existing mysql DB to auth discourse users
SSO locked me out of Discourse!
What are the rules for usernames?
Discourse single-sign-on integration with SharePoint
Discourse API - Login case SSO Provider to Consumer site
Help with Okta SSO
Merging users from different forums
Sign In Error (SSO may be enabled)
Is there a "log_in" SSO API endpoint?
ASP.NET MVC Single-Sign-On
About the sso category
SSO locked me out of Discourse!
All my SSO Users have admin privileges
User is not logged in after SSO sequence
SSO does not redirect, is canceled instead
Using existing RoR application for user auth / signup instead of discourse
Discourse SSO + normal login
Users who register on my site, register also on Discourse Vise Versa
Smoothly integrating Discourse with an existing social site
How to implement delegated SSO?
Questions about Discourse on Digital Ocean
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
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
Sign up and local authentication disappeared after enabling SSO-based authentication
Disabling all emails except those registration related?
How to control which email messages send and do not send
SSO on Discourse using Atlassian Crowd
Can't get avatar overrides to work over SSO
SSO for dummies
Redirect all users who click on domain.com/signup to a different page
Questions Regarding Account Authentication Methods
How to change login settings without being logged in?
Discourse Ruby API testing "Unknown attribute 'auth_token' for User
New users via API if allow new unchecked
Discourse as Rails engine
Using Stripe for Members only Group and Category
Login to Discourse with custom Oauth2 provider
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
RuntimeError Bad signature for payload during SSO login and signup
Disable SSO While Not Signed In
Python 3 - SSO Helper Class
Rails/Devise and SSO Question
Rails/Devise and SSO Question
Discourse integration with Grails
WP Discourse SSO Plugin
Wp-discuss integration error
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
How to use the Discourse auth system for another app?
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?
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
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
SSO with SAML (mod_shibboleth) and Flask
SSO with SAML (mod_shibboleth) and Flask
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
(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?


(Danny Dulai) #244

This part is confusing me. Can you fix the grammar issue here? “it will attempt to” doesn’t make sense after “… nonce is valid”


(Jeff Atwood) #245

It is a wiki you can edit it yourself as a non-new user. Go ahead…


(Danny Dulai) #246

Cool, I didn’t know that about editing. However, I don’t know what it is trying to say.

I believe what it is saying is: First, it’ll validate that the nonce is valid. If it is indeed valid, it will expire it and continue to attempt to match the ‘sso user info’ to the ‘discourse user’ via the 3 methods listed.

Is that right?

I’m doing a migration to SSO, and I’m trying to figure out if the email address is the ‘key’ for the user or the ‘external_id’ is the key. If my interpretation is correct, it is both, in order of external_id, and then email – but the authority on both are the SSO data.


(Kane York) #247

It’s saying that when a nonce is accepted, it is immediately forgotten and can’t be used again. It’s replay protection - someone recording all the URLs you visit can’t reuse the URL your SSO gave out and get a valid login (the nonce already got used by the legitimate user).

external_id should be the id column in your database, or an equivalent - something that is tied to the identity of the user. The email is used to match up new logins - those with an unknown external_id - with existing users in Discourse.


#248

This is how I got this to work on my rails site (which uses auth0.com, not devise, for authentication):smile:

  1. Add this gem to gemfile:
    gem ‘discourse_sso’, ‘~> 0.0.2’

  2. Follow the instructions in the answer by DanSingerman
    Incorporating Discourse SSO with Existing Rails Site with Devise - Stack Overflow
    (I replaced “authenticate_user!” with my own authentication method for auth0)

Note: If you’re using auth0, don’t use auth0-discourse plugin if you want to skip discourse’s authentication system. These instructions are for true SSO skipping discourse authentication entirely.


Change right gutter to vertical timeline + topic controls
#249

We need to do this too, if the SSO provider were able to pass back a list of groups that we want a new user to be assigned to it would be great. Is this possible at the moment or are there any techniques that can achieve the same goal?


(Ravikiran Janardhana) #250

If anyone is looking to add SSO auth to Discourse via LDAP, take a look at discourse-sso-python-ldap on github. It should be pretty straightforward.


(Adam Capriola) #251

Is there any chance we’ll be able to set a whitelist for return_path domains in the future? It would be super helpful to configure it back to my SSO provider to initiate syncing of user info like custom fields and groups.

I wish I knew enough Ruby to contribute. Maybe I could figure it out. It seems like a domains whitelist setting (like for oneboxes) is needed and then a check against these domains in session_controller.rb.


#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.


(Thomas Purchas) #255

@dmchicago and @savef
I’m currently doing this with a very crude plugin.

I use the custom fields to pass group information through using the SSO, and then run a job once a day to update peoples groups to match what’s stored in their custom fields.

On our site, most groups have an associated badge which is assigned on user info update using a SQL query. So I also use the badge added/removed triggers to get something that will sync most user groups faster.

Ideally there would be a user updated trigger which I could use, instead of this botch with badges, but I haven’t had time to write the pull request (ruby is not my language of choice so I’m having issues with simple stuff like tests).

Feel free to fork the plugin above and use it, you just need to change the group to custom fields mapping in plugin.rb, also feel free to ping me or start a new topic if you need help. Can’t make any promises, but if I have time I’ll help.


Automatic SQL badges not firing ":user_badge_granted" trigger
Category creep and information overload
#256

Thanks a lot, I won’t be looking at this properly right away, but I think we’ll be able to make use of your plugin. :+1:


#257

Are there plans to make groups accessors in SSO?


(DC) #258

Is there a way to use this signon with a native app?
eg if we want to embed discourse within a webview of an android app, but somehow pass a login token to discourse so the user is already logged in when they open the app?

In a lot of cases, webviews won’t cache (cookies etc) effectively so if the user logs in through a webview once, which is painful enough, they may have to then login again next time.

If there was a way to invisibly “force login” the user by opening discourse on a custom URL that would be an easy approach. The URL could be a secret URL only available via an authenticated API call from the app.