Discourse SSO with Firebase (Discourse Connect)

694 views Asked by At

I have a website that uses firebase authentication and I want to use these same login details for discourse, whats the best way to achieve this?

1

There are 1 answers

1
chantey On

1. Using Redirects

This is the workflow I have found the most elegant. It allows sign in from discourse with a single click as long as you are signed in on the main site, because firebase auth will auto-sign-in on visit.

The flow of data is like this:

  1. forum -> site
    • send Discourse SSO Request
  2. site -> server
    • send user token and Discourse SSO Request
  3. server -> site -> forum
    • send redirect to forum with Discourse Login

Here is the code used to create this:

  1. forum - Discourse Connect Url
https://MY-SITE.com/forum-sign-in
  1. site - page visit handler

This part should go on your website when people visit /forum-sign-in. It will automatically trigger the sso flow once they are signed in, which will happen automatically if they have previously signed in.

auth.onAuthStateChanged(async user => {
    if (!user)
//this means user was not already signed in. 
//allow the user to sign in like normal
//this callback will be called again once they do
        return
//generate firebase auth token
    const idToken = await user.getIdToken(true)
//get the search params that discourse sent
//if you're in react you should probably do useSearchParams or something
    const params = new URLSearchParams(document.location.search)
    const discoursePayload = params.get('sso')
    const discourseSignature = params.get('sig')
    const response = await fetch(`https://MY-BACKEND.com/discourseAuth`, {
        method: 'POST', //sending a json payload
        mode: 'cors', //work with another domain
        cache: 'no-cache', //send every time
        headers: { 'Content-Type': 'application/json' }, //sending a json payload
        body: JSON.stringify({
            discoursePayload,
            discourseSignature,
            idToken 
        })
    })
    const json = await response.json()
    if (json?.redirectUrl)
//lets go back to forum with credentials in the redirectUrl
//we're doing a custom redirect instead of a redirect-follow which is easier for security
        window.location.replace(json.redirectUrl)
    else 
//something went wrong
        throw new Error(`Redirect URL not found in response - ${response.status} - ${json}`)
})
  1. server - auth handler

I'm using firebase functions here, but as long as you have access to firebase-admin you can use whatever backend you like. Also this example includes some firestore stuff for usernames etc, which is not required.

import DiscourseSSO from 'discourse-sso'
import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions'

const auth = admin.auth()
const firestore = admin.firestore()
const discourse = new DiscourseSSO(ssoSecret)


export const discourseAuth = functions.https.onRequest((req, res) => {
    if (!handleCors(req, res))
        return

    //1. validate discourse payload
    const { idToken, discoursePayload, discourseSignature } = req.body
    if (!discourse.validate(discoursePayload, discourseSignature))
        return res.status(401).send(`Bad Discourse Payload: ${origin}`)

    //2. validate user
    const decodedClaims = await auth.verifyIdToken(idToken).catch()
    if (!decodedClaims)
        return res.status(401).send(`Bad Id Token: ${idToken}`)
    const { uid } = decodedClaims
    const user = await auth.getUser(uid).catch()
    if (!user)
        return res.status(401).send(`User Not Found: ${uid}`)

    //3. get user firestore (optional)
    const firestoreDoc = await firestore.collection('users').doc(uid).get()
    const userData = firestoreDoc.data()

    //4. build discourse auth body
    const q = discourse.buildLoginString({
        nonce: discourse.getNonce(discoursePayload),
        external_id: uid,
        email: user.email,
        //optional
        name: userData.displayName,
        username: userData.username,
        avatar_url:userData.avatar
    })
    const redirectUrl = `$https://forum.MY-SITE.com/session/sso_login?${q}`
    res.status(200).send(JSON.stringify({ redirectUrl }))

const handleCors = (req, res) => {
    const origin = req.headers.origin || req.header('origin')
    if(origin != 'https://MY-SITE.com'){
        res.status(401).send(`Bad Origin: ${origin}`)
        return false
    }
    res.set('Access-Control-Allow-Origin', origin)
    res.set('Access-Control-Allow-Credentials', 'true')
    res.set('Access-Control-Allow-Headers', ['Content-Type'])
    if (req.method === 'OPTIONS'){
        res.status(200).end()
        return false
    }
    return true
}

2. Using Cookies

The second method is one I had trouble with because my server is on a different domain from the client and forum. This means that Safari will treat the cookies as third party and give you a hard time when trying to pass them around. If this isn't the case for you, check out this discussion and this GitHub gist. The credentials are all the same, the only difference is how they are passed around. It is something like this:

  1. site -> server
    • send Firebase user token
  2. server -> site
    • set session cookie
  3. forum -> server
    • send cookie and discourse SSO request
  4. server -> forum
    • send Discourse Login