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 tosso_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
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
- Validate the signature: ensure that HMAC-SHA256 of
PAYLOAD
(usingsso_secret
, as the key) is equal to thesig
(sig
will be hex encoded). - Perform whatever authentication it has to
- 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
- Base64 encode the payload
- Calculate a HMAC-SHA256 hash of the payload using sso_secret as the key and Base64 encoded payload as text
- 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:
- Log the user on by looking up an already associated external_id in the
SingleSignOnRecord
model - Log the user on by using the email provided (updating external_id) (unless require_activation = true)
- 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
- Payload is validated using HMAC-SHA256, if the sig mismatches, process aborts.
- 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 examplecustom.user_field_1
can be used to set the value of theUserCustomField
that has the nameuser_field_1
. - You can pass
avatar_url
to override user avatar (SiteSetting.sso_overrides_avatar
needs to be enabled). Avatars are cached so passavatar_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
. Be sure to 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:
-
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)