Discourse SSO with Auth0.

from Leo Giovanetti.

TL;DR

You can achieve a very seamless SSO experience with Discourse and Auth0 using one of the best features Auth0 has: rules. Check the “Implementation” section for the code.

Background

I don’t know you but I discovered Discourse a while back when I selected it to be part of the online platform for the new political party we are creating in Uruguay based on ICT.

We needed an easy way people can engage talking and discussing about ideas to be part of the Government Programme. It needed to be something familiar for people, like a forum but adapted to this era. It fitted perfectly.

Now, I also had the fantastic idea (modesty aside) to use a service to handle all the users in order to have a consistent experience throughout all the tools in the platform. Something easy to configure, vast to customize and most importantly, free of charge, as we are just growing and we just get funding based on donations (check out OpenCollective, very recommended). That’s why I decided to use Auth0. Very neat service which supports open source projects like the political party I work for and support.

So, anyway, although there is a nice Discourse plugin to hook up Auth0 authentication, I always struggled with how it handled the creation of a user which needed to be re-created in Discurse database to work. What’s the point of having a super user handling service if at the end the user needs to tackle a user creation dialog on Discourse?

Understanding the problem

After a while trying to understand how stuff worked on both ends, Discourse and Auth0, I got the idea of implementing what Discourse needed on Auth0’s side.

When I say a while trying to understand how stuff worked on both ends, I mean submitting a bunch of questions which got very good answers most of the times.

These are some of my attemps:

Link Preview

Setup DiscourseConnect - Official Single-Sign-On for Discourse (sso) - #345 - Integrations - Discourse Meta

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 a…

 https://meta.discourse.org/t/official-single-sign-on-for-discourse-sso/13045/345?u=leog
Link Preview

Customize login modal · Issue #20 · auth0/discourse-plugin · GitHub

Hello! This is a great plugin, I'm hoping to use it on our Discourse instance for our political party on Uruguay based on IT. I was wondering if there is any way to customize the lock instance to match our desired look and feel used on o...

 https://github.com/auth0/discourse-plugin/issues/20?ref=leog.me
Link Preview

SSO vs Oauth2 difference? - #3 by leog - SSO - Discourse Meta

Hello I’m working on a website with django CMS. I would like to link the user accounts of my site to discourse. In the first sept, I wish understand the différence between SSO and Oauth2 I thought it was the same thi…

 https://meta.discourse.org/t/sso-vs-oauth2-difference/76543/3?u=leog&ref=leog.me

And that last one was key for the findings relevant to this blog post 💡

Thanks to Michael Brown’s super neat reply with the following diagrams, I got the realization that something could be done from the authentication service side.

How Discourse handles OAuth2 authentication
How Discourse handles OAuth2 authentication
How Discourse handles SSO with whatever authentication mechanisms is behind (OAuth2 in this case)
How Discourse handles SSO with whatever authentication mechanisms
is behind (OAuth2 in this case)

What a journey! But anyway, here comes the code.

Implementation

First of all, a big shoutout to Johan Jatko for creating the discourse-sso node.js package that simplified a lot of code for me to implement this.

This code goes into a new Auth0 rule which is going to be run when a user signs in from Discourse using SSO:

function (user, context, callback) {
  // Check whether the Auth0 client is the one we want to apply this rule to
  if(context.clientID === "CLIENT_ID") {
    
    // Check out Discourse's SSO implementation requirements already in discourse-sso package
    // at https://meta.discourse.org/t/official-single-sign-on-for-discourse-sso/13045#heading--implement
    var discourse_sso = require('discourse-sso');
    
    // Setup sso_secret variable on your client variables on Auth0 so you don't need to have it inline in your code
    var sso = new discourse_sso(context.clientMetadata.sso_secret);
    
    // Validate the query payload with its signature (it uses the sso_secret passed to the discourse_sso instance)
    if(sso.validate(context.request.query.sso, context.request.query.sig)) {
      
      // Extract nonce information 
      var nonce = sso.getNonce(context.request.query.sso);
      
      var userparams = {
        // Required, will throw exception otherwise 
        "nonce": nonce,
        "external_id": user.user_id,
        "email": user.email,
        // Optional
        "username": user.nickname,
        "require_activation": !user.email_verified,
        "suppress_welcome_message": true
      };
    
      var q = sso.buildLoginString(userparams);
    
      context.redirect = {
          url: "DISCOURSE_URL/session/sso_login?" + q
      };
    }
  }
  
  callback(null, user, context);
}

Please replace CLIENT_ID and DISCOURSE_URL with your information. And on line 10, context.clientMetadata.sso_secret is a variable (can be a just a string with the value but avoid doing that as much as possible) set on your Auth0 Client advanced configuration, like this:

Auth0 Client Advanced settings to setup sso_secret
Auth0 Client Advanced settings to setup sso_secret

Then, in your Discourse instance, under Settings > Login, you will find these options:

Discourse Admin Login SSO options
Discourse Admin Login SSO options

The important fields are:

The following ones are just to customize how SSO information from Auth0 can override Discourse information. I suggest enabling verbose sso logging so you can debug better any potential issues related to SSO.

Note: The rule you create to handle this flow will execute and then as the authentication attempt will stop there (no call to /continue?state=STATE_ID will happen), no other subsequent rule will execute, so mind the order of the rules.

Hope it helps someone as lost as I was 👍

And continue giving me feedback, don’t hesitate to let me know if you can think of any improvement.