Use Discourse as an identity provider (SSO, DiscourseConnect)

So you want to use Discourse as an identity provider for your own web app? Great! Let’s get started.

Enable DiscourseConnect provider setting

Under Discourse admin site settings (/admin/site_settings) enable setting enable discourse connect provider and add a secret string to discourse connect provider secrets (used to hash SSO payloads).

Implement DiscourseConnect in your web app:

  • Generate a random nonce. Let’s call this value NONCE. Save it temporarily so that you can verify it with the nonce value that will be returned in the response.

  • Create a new payload with the NONCE and a RETURN_URL (where Discourse will redirect the user after verification). Payload should look like: nonce=NONCE&return_sso_url=RETURN_URL. The host of RETURN_URL must match against the domain pattern you used when configuring discourse connect provider secrets.

  • Base64 encode the above raw payload. Let’s call this payload as BASE64_PAYLOAD

  • URL encode the above BASE64_PAYLOAD. Let’s call this payload as URL_ENCODED_PAYLOAD

  • Generate a HMAC-SHA256 signature from BASE64_PAYLOAD using your sso provider secret as the key, then create a lower case hex string from this. Let’s call this signature as HEX_SIGNATURE

Send auth request to Discourse

Redirect the user to DISCOURSE_ROOT_URL/session/sso_provider?sso=URL_ENCODED_PAYLOAD&sig=HEX_SIGNATURE

Get response from Discourse:

If the above steps are done correctly Discourse will redirect the logged-in user to the provided RETURN_URL. You will get query string parameters with sig and sso along with some user info. Now follow below steps:

  • Compute the HMAC-SHA256 of sso using sso provider secret as your key.

  • Convert sig from it’s hex string representation back into bytes.

  • Make sure the above two values are equal.

  • Base64 decode sso; you’ll get the passed embedded query string. This will have a key called nonce whose value should match the nonce passed originally. Make sure that this is the case, and be sure to delete the nonce from your system.

  • You’ll find this query string will also contain a bunch of user information. Use as you see fit.

That’s it. By now you should have set up your web app to use Discourse as SSO provider!

More Parameters, More Options

In addition to nonce and return_sso_url, the request payload has two additional optional parameters.

  • prompt: If prompt=none, then the SSO request is treated as a “just checking” request. If the browser/device is already logged-in to Discourse, Discourse will return a successful SSO response bearing user authentication information, as usual. If the browser/device is not already logged-in, then Discourse will not ask the user to log in, and will immediately return an SSO response bearing the parameter failed=true instead of user information. This provides a mechanism to query if the user is logged-in, without ever directing the user to a login dialog if they are not.

  • logout: If logout=true, then the SSO request becomes a logout request. If a user is logged-in to Discourse on that browser/device, they will be logged out of that device. In either case, Discourse will immediately redirect back to the return_sso_url, without sso or sig added to the query string.

prompt=none and logout=true are mutually-exclusive; it makes no sense to provide both in the same request.

sso= Payload Reference

Request parameters:

  • nonce: (string, required) a securely-generated random string
  • return_sso_url: (string, required) the URL to redirect back to with the response
  • prompt: (string, optional) If none, probe authentication status without prompting user to log-in.
  • logout: (boolean, default false) If true, log the user out of Discourse.

Result parameters:

  • There is no sso= payload, or signature, in response to a logout request, just a redirect to the request’s plain return_sso_url.

  • The result payload for a login request will always contain the nonce, reflected from the request.

  • The result payload will also reflect any other request parameters. Do not rely on this behavior; it is not necessarily intentional and not a guaranteed aspect of the API. (E.g., why is the return_sso_url parameter copied into the payload that is sent to the return_sso_url?)

  • If the request failed to authenticate a user, the result payload will contain failed=true.

  • If the request succeeded in authenticating a user, the result payload will contain user credentials/information:

    • external_id: (integer) Discourse id
    • username: (string) username/handle
    • name: (string) user’s real name
    • email: (string) email address
    • avatar_url: (string) URL to the original, unscaled image as uploaded by user
    • admin: (boolean) true if user is an Admin, otherwise false
    • moderator: (boolean) true if user is a Moderator, otherwise false
    • groups: (string) comma-separated list of groups (by name) to which the user belongs

    name and avatar_url may be absent if the user never provided a real name or uploaded an avatar image. (Any element with a nil value within Discourse will be omitted from the response.)

Discourse official “Using Discourse as identity provider” implementations:

Community contributed “Using Discourse as SSO provider” implementations:

Last edited by @JammyDodger 2024-05-26T07:20:55Z

Check documentPerform check on document:
61 Likes

Great how to, thanks.

FYI, the term ‘nonce’ has an unfortunate meaning in Britain.

10 Likes

I wrote a class to implement the process in Ruby. I use it to work with devise in my web application.

https://github.com/gogo52cn/sso_with_discourse

3 Likes

sso seems to have a trailing newline which needs to be included when sent to the HMAC function, so it’s important to make sure that SSO consumer applications don’t strip whitespace from these query arguments.

1 Like

Our app users get their own subdomains. Is there a way to implement this with variable SSO urls?

interesting, this is tricky you would need to monkey patch this in, or better still make core extensible in this way and add a plugin.

3 Likes

Hello,

I have created an Erlang implementation for encoding/decoding the payloads described in this specification. You can find it here:
https://github.com/reverendpaco/discourse-as-sso-erlang
Feedback is welcome.

Thanks to @mpalmer on another thread for setting me straight.

As I put together this implementation, I realized that the specification does not say too much about a few things:

  1. the user information that is returned in the query parameters,
  2. what happens when a user is not found

When I got around to testing I found the answers, and then confirmed by looking at the code, here: https://github.com/discourse/discourse/blob/master/app/controllers/session_controller.rb#L33

I shall assume that in regards to 1. that while new user information may be added, that none of these values (“name”,“username”,“email”,“external_id”, etc) will be removed. This is just as important, contractually as what is described in the main post.

One piece of feedback I’d like to give the Discourse team is that it would be nice to add a means to optionally return back to the calling application in the case of a missing user.

Currently, at line 51 a non-logged-in or non-registered user will be forwarded to the Discourse login page. While this can be useful, I would rather programmatically have the option to learn that this person has not yet logged in (or registered) and give them the opportunity on my site to continue anonymously.

I can imagine something like this:

DISCOURSE_ROOT_URL/session/sso_provider?
sso=URL_ENCODED_PAYLOAD&sig=HEX_SIGNATURE&
returnBackIfUserMissing=true

and then the Discourse site sending back to the return_sso_url with either a special header, or an attribute, rather than redirecting to /login.

This change should be backwards compatible.
I’m new here, so if this is something that could be contributed via a pull-request, please tell me and I could take a shot at it next week.

thanks,
daniel

5 Likes

Sure, totally open to add another option to the payload you send us for create_new=false PR welcome.

2 Likes

added a PR for this

https://github.com/discourse/discourse/pull/4211

Commit-Message:

Implemented an optional 'no_user_found_return_sso_url' parameter to be called
 by the client when client is using Discourse as an SSO and wants Discourse to 
redirect back to a place of the client's choosng when a user is not found.

Currently, the Discourse as an SSO implementation checks the cookies _t and
 _session_forum to see if the user is registered and present in the database 
(_t is the token that is located in the users table). If a user is not found, 
the current implemenation forwards to the forum's /login URL. This behavior 
may be what the client wants, but it would be good to give an option to the 
client to send somewhere else.

This commit allows the client to embed an optional 'no_user_found_return_sso_url' 
parameter in the payload, prior to base64 and URL-encoding. If the Discoure SSO
endpoint detects that this parameter is present in the payload (and has a non-empty
value) the Discourse server will redirect to this new location if it does not detect the 
user. If this parameter is not present, then the redirection to /login will take place 
as it currently does.

Additionally, as the client may choose to use the same URL for
'no_user_found_return_sso_url' as for 'return_url', this commit introduces a new 
query-string name-value pair to be sent back to the client 'no_user_found_return_sso_url' 
location. This parameter 'user_found' will ALWAYS be sent back to the client, either when 
the user is found and 'return_url' is used or when the user is not found 
'no_user_found_return_sso_url' is used (values will be 'true' and 'false' respectively).

thanks,
daniel

4 Likes

Nice work :slight_smile:

redirect_to can be routed to a url with parameters. But isn’t a create_new=false enough? Or whatever name it is. You’ll get nonce and the flag back.

I wanted to give the client the flexibility of sending a failure to a different URL than the success url (return_url). With complicated SSO architectures, this might be a requirement. (Selfishly, I wanted to make my return endpoint code less convoluted – i.e. on my side I have two codebases at /sso for success and /sso_failure for failure).

Implicitly there are already two URLs (return_url for success and ‘/login’ for failure) – I didn’t want to lose that.

So just to be clear, the payload you send to the Discourse endpoint should have both return_url and no_user_found_return_sso_url if you want Discourse to send it back when no user is found. It should have return_url only if you are ok with Discourse forwarding to /login.

Should look like this:

1.If you want Discourse to forward to login if no user found, then:

PAYLOAD = return_url=mydomain.com/clientendpoint&nonce=xE787euK
2. If you want Discourse to send back to you to the same endpoint for failure as success:

PAYLOAD = no_user_found_return_sso_url=mydomain.com/clientendpoint&return_url=mydomain.com/clientendpoint&nonce=xE787euK
3. If you want Discourse to send back to you to a different endpoint as success:

PAYLOAD = no_user_found_return_sso_url=sub.mydomain.com/handleNewUser&return_url=mydomain.com/clientendpoint&nonce=xE787euK

Potentially a bit over-engineered, but that was my thought process.

1 Like

To be clear, it’s not user_not_found, it’s not logged in.

It’s about protocol not engineering in the first place. Given two endpoints, you have to deal with two possible endpoints. In both cases you must validate the nonce and destroy it. It’s a hurdle not benefit.

In extreme case, a client can ask Discourse several times with/without session (current_user), thus you would get n responses in each endpoints. It would be twice as hard to secure it due to changing cookies on your end.

2 Likes

Completely agree on protocol over engineering. Hence, my submission.

The semantics are equivalent to an if-else block, or more generally a case/switch block. result_url is our true condition, and user_not_found_url is our else block. If we didn’t provide it, we would force the client to have to deal with the ‘failure’ condition in the same codebase/endpoint – and for 90% of the people, this will be fine.

90% of people who want Discourse to return back to them will set return_url and user_not_found_url to be exact same thing. This bears repeating, and really renders the motivations for the 10% moot.

90% of the time you will make return_url and user_not_found_url the exact same thing. These 90%-ers will use their JSESSIONID/ PHPSESSID/ ASPSESSIONID /_session to look up the returned encoded nonce in their framework-supplied session-store and engineer accordingly.

7% of people will be happy to pass it to a different URL on their same app-server, which does some decoration (servlet-chains anyone?) or routing, but still looks up the nonce in their session.

3% will have some complicated polyglotish system that uses a distributed session store (like memcache) to store sessions for different app servers implemented in different legacy codebases. It’s up to them to store/invalidate the nonce across these different systems.

I realize I might not have been completely clear, but the user_not_found_url still receives the sso and sig parameters, just like the return_url.

So, if you are the 90% scenario, when you get the payload and verify/decode it, you will find the parameter user_found=false|true to know why it’s coming back to you.

2 Likes

@fantasticfears, as per your recommendation on the PR, I have renamed the attribute from user_not_found_url to return_sso_unlogged_in_url.

1 Like

3 posts were split to a new topic: Can Discourse be used as an OAuth provider?

Great job !

Do you know how it possible to adapt it to GitLab in order to allow the Discourse users to directly login there?

2 Likes

Hey folks, I could not find anything, so I created a npm for Discourse auth using Passport for node.js.

It’s here, and in npm if anyone needs it:

https://github.com/edhemphill/passport-discourse

(really appreciate the team’s work, we’ve been a user since the very early days @ talk.wigwag.com)

5 Likes

@sam,

I have submitted a reply back to your rejection of the PR:
https://github.com/discourse/discourse/pull/4211

As I have been relying on my patched version of discourse for these months, I would be interested in convincing you as to my need.

If this PR is going to be rejected, could you explain what I can do to get the functionality that I need?

Does anyone know of a wordpress plugin that works with discourse as the SSO provider?

1 Like