sam | 2023-07-14 11:34:46 UTC | #1
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](https://discourse.org/pricing).
> :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: https://meta.discourse.org/t/vk-com-login-vkontakte/12987
### Enabling DiscourseConnect
To enable DiscourseConnect you have 3 settings you need to fill out:
![Screenshot 2021-02-11 at 12.40.34|690x200](upload://tGxwEdmQ5KxyB0XC0fYz6X05lu7.jpeg)
`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: https://meta.discourse.org/t/what-to-do-when-you-have-locked-yourself-out-by-invalid-sso-configuration-or-read-only-mode/89605
### 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][1], 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][2] 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`.
![Screenshot 2021-02-11 at 12.35.15|690x69, 100%](upload://yhwgzQ9EhzbpnbadTHxZIKD5nVq.jpeg)
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:
https://github.com/discourse/discourse/blob/main/lib/discourse_connect_base.rb
A trivial implementation would be:
```ruby
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 https://meta.discourse.org/t/sync-discourseconnect-user-data-with-the-sync-sso-route/84398 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:
```ruby
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](https://github.com/discourse/discourse_api) can be used for SSO. Have a look at the SSO code in its [examples directory](https://github.com/discourse/discourse_api/tree/master/examples) to see a basic implementation.
- Our [WordPress plugin](https://wordpress.org/plugins/wp-discourse/) 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](https://meta.discourse.org/c/sso/24).
### 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`
### 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:
- Every time the DiscourseConnect process is initiated we will log a warning to the log with a full dump of the DiscourseConnect payload
- Every time a user fails to complete DiscourseConnect (due to nonce expiring or ip block)
[1]: http://en.wikipedia.org/wiki/Cryptographic_nonce
[2]: http://en.wikipedia.org/wiki/Base64
[3]: http://en.wikipedia.org/wiki/Hash-based_message_authentication_code
-------------------------
barmaxech | 2024-03-04 15:25:06 UTC | #568
"There is a problem with your account. Please contact the site's administrator."
I connected sso for our discourse.
How to debug this problem? Some accounts from our site can't log in. There is no log about the issue. It's logged only when it's redirected from discord forum to our site.
-------------------------
simon | 2024-03-04 19:14:26 UTC | #569
[quote="Barmaxech, post:568, topic:13045, username:barmaxech"]
“There is a problem with your account. Please contact the site’s administrator.”
[/quote]
Have a look at this topic:
https://meta.discourse.org/t/debug-and-fixing-common-discourseconnect-issues/103496
Edit: searching for the phrase "There is a problem with your account. Please contact the site’s administrator" would ideally return the topic that I linked to, but it doesn't. The problem is that the topic shows the error message in a screenshot instead of as text.
-------------------------
barmaxech | 2024-03-05 13:59:10 UTC | #570
I looked at it, but it doesn't help. Verbose logging is on. I don't see an error but I still get 'There is a problem with your account. Please contact the site's administrator.' issue.
![image|690x272](upload://A3TQhFP6cqaxY7ADNTPLEjyWl1y.png)
-------------------------
oja95 | 2024-03-05 14:01:21 UTC | #571
Is there a more up-to-date documentation about which data fields my SSO endpoint can return? I can see that there are some options for website, location and some other options that weren't specified in the top post in the list of data keys that Discourse understands:
![image|614x134](upload://rms42BgZJiQSl9cQo6I7ZaHzCt1.png)
-------------------------
simon | 2024-03-05 18:21:56 UTC | #572
[quote="Barmaxech, post:570, topic:13045, username:barmaxech"]
I don’t see an error
[/quote]
Try entering "Record was invalid" (without the quotes) into the search form:
![image|481x500](upload://bmH3H7k6Zi3MTJHLmiCDbmOSI46.png)
If you're still not seeing it, make sure a user who is reporting the issue has attempted to login since you enabled the verbose logging setting.
-------------------------
simon | 2024-03-05 18:30:29 UTC | #573
[quote="Tiit, post:571, topic:13045, username:oja95"]
Is there a more up-to-date documentation about which data fields my SSO endpoint can return?
[/quote]
If it's not documented anywhere else, the `ACCESSORS` list at the top of this file shows the available fields:
https://github.com/discourse/discourse/blob/main/lib/discourse_connect_base.rb#L7-L37
-------------------------
barmaxech | 2024-03-06 07:27:15 UTC | #574
I already checked. It was me, I was trying different users. Sometimes they work, sometimes I get this error. And I don't see any logs about this. No 'Record was invalid'. I saw it only one time when email was ending on @gmail.com_old, it was invalid email of our test user.
-------------------------
simon | 2024-03-06 09:36:15 UTC | #575
[quote="Barmaxech, post:574, topic:13045, username:barmaxech"]
I already checked. It was me, I was trying different users. Sometimes they work, sometimes I get this error.
[/quote]
I'd expect there to be some kind of error in the logs that starts with "Verbose SSO log"
Can you confirm that when the issue occurs, users are seeing this message on the login page: "There is a problem with your account. Please contact the site’s administrator."?
-------------------------
barmaxech | 2024-03-06 14:56:04 UTC | #577
Yes. I encountered another error with too many regs by one ip. So I know that in case of an error, Verbose SSO log shoud come up with description. With this “There is a problem with your account. Please contact the site’s administrator” message, it doesn’t.
-------------------------