Use Discourse as an identity provider (SSO, DiscourseConnect)

It seems that trying to authenticate into discourse-auth-proxy with non-ASCII usernames or group names (Chinese in my case) leads to an error due to cookies cannot contain these characters. This is my fix: (disclaimer: I’m not really familiar with golang)

diff --git a/main.go b/main.go
index 1b1dc28..18f8c9e 100644
--- a/main.go
+++ b/main.go
@@ -154,7 +154,12 @@ func redirectIfNoCookie(handler http.Handler, r *http.Request, w http.ResponseWr
        var username, groups string
 
        if err == nil && cookie != nil {
-               username, groups, err = parseCookie(cookie.Value, config.CookieSecret)
+               var value string
+               value, err = url.QueryUnescape(cookie.Value)
+               if err != nil {
+                       return
+               }
+               username, groups, err = parseCookie(value, config.CookieSecret)
        }
 
        if err == nil {
@@ -224,7 +229,7 @@ func redirectIfNoCookie(handler http.Handler, r *http.Request, w http.ResponseWr
                cookieData := strings.Join([]string{username, strings.Join(groups, "|")}, ",")
                http.SetCookie(w, &http.Cookie{
                        Name:     cookieName,
-                       Value:    signCookie(cookieData, config.CookieSecret),
+                       Value:    url.QueryEscape(signCookie(cookieData, config.CookieSecret)),
                        Expires:  expiration,
                        HttpOnly: true,
                        Path:     "/",
1 Like

Thanks for reporting! Would you mind making a PR against the github repository so that we can merge this change into the official version?

3 Likes

I was able to use this to create an account linking system for my Minecraft server! Thought I’d share what it looks like! This was my first time working with Discourse SSO, so I might’ve overcomplicated everything. However, it works, which is the main thing.

4 Likes

Hi i did all these correctly. but when the user is not logged in discourse it will show user login popup. when I fill username and password, it does not redirect me back to return_url. can you please help me?

I assume the nonce is there to prevent replay attacks. I read online that replay attacks aren’t possible with HTTPS, which is what I’m using. So do I still need to do the nonce? I ask because I’m not sure where to store it. Does it make sense to store it as a secure, plain text cookie in the user’s browser? and then read it from the browser along with the return payload?

This library, which is linked from the original post GitHub - ArmedGuy/discourse_sso_node: npm package for Discourse SSO login features. doesn’t use the nonce when validating the user.

Yes, you still need to validate the nonce because it prevents reusing payloads that Discourse sends when it redirects users back to your site.

For example, let’s your site has some content behind a paywall that only members of the subscribers group on Discourse can access and you use the groups field in the payload that Discourse sends to your site to display the paid content only to members of the subscribers group. If you don’t validate the nonce, a user who is no longer in the subscribers could use an old payload from when they were a member to login to your site and see the paid content.

It’s best to store the nonce in a database with a short expiration date and delete the nonce from the database as soon as it’s used. However, if you can’t use a database, then you can use a cookie to store the nonce, but you need to do some additional steps to prevent payload reuse:

  1. attach an expiration date to the nonce when you generate it, for example 10 minutes from the current time
  2. sign the whole cookie (nonce + expiration date) to prevent users from modifying the nonce and/or the expiration date
  3. verify the signature of the cookie and ensure the nonce isn’t expired

That should give you a good enough protection against payload reuse. Keep in mind that technically it remains possible to reuse a payload, but it’ll be limited to a 10 minutes window instead of forever.

A simpler solution that doesn’t need a cookie is to include the expiration date in a custom field in the payload that you generate. Then when Discourse redirects users back to your site with a payload, your custom fields will be included and you can retrieve the expiration date and you verify it’s not expired. To include a custom field in the payload, you need to include a field prefixed with custom., so your payload would look like this:

nonce=NONCE&return_sso_url=RETURN_URL&custom.expiration_date=TIMESTAMP
3 Likes

You could also store the nonce in the session, that will prevent the user from tampering with it as well.

3 Likes

Just circling back to this thread years later

Can someone tell me (@pfaffman or @tobiaseigen or @iamntz) what the Discourse SSO provider returns? I know I can “try it and see” but it would be nice to have it documented. The github PHP sample code doesn’t trally have any other fields even mentioned.

Ideally, it would send the same fields as when Discourse uses the external script for SSO, such as external id, email, username, name, avatar photo etc. So we can import this and create a user on our side!

Does it also tell Wordpress the email?

How about groups, badges etc? Can we find this information by making REST calls?

Finally, what about the user’s private messages and other stuff? I guess if Discord was an oAuth provider and allowed our apps to consume this sruff, that would be awesome.

When trying to enable Discourse Connect I get this error:
enable_discourse_connect: You cannot enable DiscourseConnect and invite only at the same time.

Any ideas?

I see you have asked the same question here: Setup DiscourseConnect - Official Single-Sign-On for Discourse (sso) - #537 by Roie_Natan. If you are trying to configure the enable discourse connect setting and not the enable discourse connect provider setting, the other topic is the correct place to ask your question.

The enable discourse connect provider setting is for when you want to use your Discourse site as the identity provider for another site. The enable discourse connect setting is for when you want to log users into Discourse via an external site.

1 Like

I’ve implemented the procedure in Python for a Flask application I am building. Here is some boilerplate code for anyone who needs it. The steps laid out in this topic were pretty simple to follow but I’m not a security specialist so if I overlooked anything please let me know!

2 Likes

We are trying to implement using Discourse as an SSO provider and what we do not understand is how does Discourse know which user needs to be verified? So the directions say: “Create a new payload with nonce and return url”. But when you post this via a fetch to Discourse, how does Discourse know what user to check to see if they are logged in? Sorry, if this sounds like a stupid question, but I just cant understand how this is working and I’ve worked with alot of Auth systems over the years, so somewhat familiar. Is the user email we are trying to check the login status for, need to be included in the payload sent to Discourse? If so, what is the exact structure of the payload that needs to be sent to Discourse. If not, again what is Discourse is checking exactly? My assumption is that we ask the user for their email on our end, and then send the payload with email to Discourse to see if that particular user is logged in, but this is not wha the directions say, so I’m totally confused. thanks for any help.

Never mind. We figured this out. We thought the SSO URL need to be sent as a POST request to the Discourse instance and then receive a response. Now we see this is a redirect to Discourse, and then Discourse then redirects back to our site. So it’s clear now what to do. Sorry for previous post.

3 Likes

FYI/FWIW: I submitted a PR to allow for a prompt=none parameter in the auth request. Similar to a feature in the OpenID Connect protocol, this allows an SSO consumer to probe whether or not a user/client is already logged in, without sending them to a login dialog if they are not.

The PR has been waiting for a final review by someone on the Discourse team for about 8 weeks now; it does seem quite a bit longer than I would expect. :crying_cat_face:

5 Likes

Hi @mdoggydog - sorry for the very long delay here!

I’ve just reviewed and merged the PR - thanks for the contribution! :raised_hands:

3 Likes

Yay! Thank you, @david.

As promised, I just updated the wiki article here to include a description of the new parameter (and the earlier new logout parameter, and to fix some minor typos/grammar, and to add a reference section documenting the sso= payload as I understand it from having dug around in the source code).

2 Likes

I want to stop using our website to serve as the SSO for Discourse and instead use Discourse’s built-in login tools to limit access to certain materials on our website.

I think the right tool to use is: GitHub - discourse/discourse-auth-proxy: An http proxy that uses the DiscourseConnect protocol to authenticate users

I haven’t found any extensive instructions on how to use it.

Can I install that in the same DigitalOcean droplet as our Discourse site, or do I need to host it somewhere else?

Edit: bolded my question :slight_smile:

1 Like

Any help on the above question? Use Discourse as an identity provider (SSO, DiscourseConnect) - #148 by alehandrof

I’m setting as the return_sso_url a URL which itself has a query parameter:

http://localhost:7000/completeLogin?returnto=%2F

The payload I send to /session/sso_provider as the sso parameter looks like this prior to base64-encoding:

nonce=ENIwf0bElViDu325dTd6&return_sso_url=http://localhost:7000/completeLogin?returnto=%2F

The URL Discourse actually redirects to after authentication is this (with the sso and sig parameters abbreviated):

http://localhost:7000/completeLogin?returnto=/&sso=...&sig=...

What surprises me here is that the query string I set for return_sso_url appears to have been urldecoded by something, because it has returnto=/ instead of returnto=%2F. The value of return_sso_url I find inside sso after base64-decoding it also has a slash instead of %2F.

Is that what I should expect to happen? (If so, why?) Is this a bug in Discourse?

What’s the reason that the sso payload contains avatar_url instead of avatar_template as is returned in /u/{username}.json and /session/current.json?

avatar_url isn’t present for uses who haven’t set an avatar, whereas avatar_template contains the leter_avatar_proxy path actually used in Discourse to show avatars for those users, and avatar_url points to the raw avatar image instead of one scaled to the desired size for uses who have set an avatar.

It seems to me that avatar_template is what anyone who intends to use the avatar information contained in sso will want—but then will need to make an additional API request to get it.