简体   繁体   中英

Discourse SSO with Firebase (Discourse Connect)

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. 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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM