Skip to content

Instantly share code, notes, and snippets.

@tegument
Last active March 11, 2022 04:13
Show Gist options
  • Save tegument/f60ef0bafac80faf7522f3a29fa04435 to your computer and use it in GitHub Desktop.
Save tegument/f60ef0bafac80faf7522f3a29fa04435 to your computer and use it in GitHub Desktop.
const functions = require('firebase-functions')
const admin = require('firebase-admin')
const express = require('express')
const app = express()
const auth = admin.auth()
const cors = require('cors')
const cookieParser = require('cookie-parser')
const DiscourseSSO = require('discourse-sso')
const env = functions.config()
const __DEBUG__ = false
/*
-- Set firebase function environment specific variables via commandline (or hard code them here):
firebase functions:config:set discourse.sso_secret='your-super-secret-secret'
{
"sso_secret": "your-super-secret-secret",
"api_url": "https://forum.domain.com/",
"sso_domain": "domain.com",
"api_key": "--your-api-key-here--", // not used for SSO, used elsewhere
"api_username": "admin" // not used for SSO, used elsewhere
}
*/
const sso = new DiscourseSSO(env.discourse.sso_secret)
const domain = env.discourse.sso_domain
const siteUrl = `https://${domain}/`
const forumUrl = `https://forum.${domain}/`
const loginUrl = `${siteUrl}login`
const whitelist = [
`http://localhost:3000`,
`https://${domain}`,
`https://forum.${domain}`
]
var corsOptionsDelegate = function (req, callback) {
var corsOptions
if (whitelist.indexOf(req.header('Origin')) !== -1) {
corsOptions = {
origin: true,
credentials: true // Must be true so that session cookies can be set.
} // reflect (enable) the requested origin in the CORS response
} else {
corsOptions = { origin: false } // disable CORS for this request
}
callback(null, corsOptions) // callback expects two parameters: error and options
}
/*
Example of client login using Firebase Auth and posting to /session endpoint:
After using Google Firebase Login you post the idToken to the /session endpoint:
Example:
function postData (url = ``, data = {}) {
// Default options are marked with *
return fetch(url, {
method: "POST",
mode: "cors",
cache: "no-cache",
credentials: "include", // SUPER IMPORTANT, requires cors config (included)
headers: {
"Content-Type": "application/json"
},
redirect: "follow",
body: JSON.stringify(data),
}).then(response => response.json());
}
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION)
.then(function () {
return firebase
.login({ provider: 'google', type: 'popup' })
.then(credential => {
credential.user.getIdToken(true).then(function (token) {
// var formData = new FormData();
// formData.append("idToken", token);
postData('https://us-central1-FIREBASEPROJECTNAME.cloudfunctions.net/sso/session', { idToken: token })
.then(data => console.log(JSON.stringify(data))) // JSON-string from `response.json()` call
.catch(error => console.error(error))
})
})
.catch(err => showError(err.message))
})
.catch(function (error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
})
*/
// This endpoint receives idToken after logging in using firebase (above example)
// https://us-central1-FIREBASEPROJECTNAME.cloudfunctions.net/sso/session
app.post('/session', cors(corsOptionsDelegate), function (req, res) {
if (__DEBUG__) console.log(env.discourse)
// Get the ID token passed
var idToken = req.body.idToken
// Set session expiration to 5 days.
var expiresIn = 60 * 60 * 24 * 5 * 1000
// Create the session cookie. This will also verify the ID token in the process.
// The session cookie will have the same claims as the ID token.
// We could also choose to enforce that the ID token auth_time is recent.
if (__DEBUG__) console.log(`/sso/session - idToken: ${idToken}`)
auth.verifyIdToken(idToken)
.then(function (decodedClaims) {
// In this case, we are enforcing that the user signed in in the last 5 minutes.
if (new Date().getTime() / 1000 - decodedClaims.auth_time < 5 * 60) {
return auth.createSessionCookie(idToken, { expiresIn: expiresIn })
}
throw new Error('UNAUTHORIZED REQUEST!')
})
.then(function (sessionCookie) {
if (__DEBUG__) console.log(`sessionCookie: ${sessionCookie}`)
// Note httpOnly cookie will not be accessible from javascript.
// secure flag should be set to true in production.
res.cookie('session', sessionCookie, { maxAge: expiresIn, httpOnly: true, secure: true }) // set secure: false to test localhost
res.end(JSON.stringify({ status: 'success' }))
})
.catch(function (error) {
if (__DEBUG__) console.log(error)
res.status(401).send('UNAUTHORIZED REQUEST!')
})
})
// This endpoint is what you use in the discourse admin for: "sso url"
// https://us-central1-FIREBASEPROJECTNAME.cloudfunctions.net/sso/discourse
// IMPORTANT: Updated URL to match the url of your firebae project.
app.get('/discourse', cookieParser(), function (req, res) {
const sessionCookie = req.cookies.session || ''
// Verify the session cookie. In this case an additional check is added to detect
// if the user's Firebase session was revoked, user deleted/disabled, etc.
if (__DEBUG__) console.log(`/sso/discourse - sessionCookie: ${sessionCookie}`)
if (__DEBUG__) console.log(`auth.verifySessionCookie`)
auth.verifySessionCookie(sessionCookie, true /** checkRevoked */)
.then((decodedClaims) => {
// once we are here the user cookie is known to be valid and we can extract the uid
var uid = decodedClaims.uid
var payload = req.query.sso // fetch from incoming request
var sig = req.query.sig // fetch from incoming request
if (__DEBUG__) console.log(`/sso/discourse -uid: ${uid}`)
if (__DEBUG__) console.log(`/sso/discourse -payload: ${payload}`)
if (__DEBUG__) console.log(`/sso/discourse -sig: ${sig}`)
if (sso.validate(payload, sig)) {
// valid sso, make sure the user is valid
auth.getUser(uid)
.then(function (userRecord) {
// Successfully fetched user data
var nonce = sso.getNonce(payload)
var userparams = {
// Required, will throw exception otherwise
'nonce': nonce,
'external_id': uid,
'email': userRecord.email,
// Optional - could pull these from DB values
'username': userRecord.displayName,
'name': userRecord.displayName
}
var q = sso.buildLoginString(userparams)
if (__DEBUG__) console.log('user authed and signed in through SSO, redirecting')
res.redirect(`${forumUrl}session/sso_login?${q}`)
})
.catch(function (error) {
if (__DEBUG__) console.log('Error fetching user data:', error)
if (__DEBUG__) console.log('redirecting to login because of invalid user data')
res.redirect(loginUrl)
})
} else {
if (__DEBUG__) console.log('unable to validate discourse payload, redirecting to login')
res.redirect(loginUrl)
}
}).catch(error => {
// Session cookie is unavailable or invalid. Force user to login.
res.redirect(loginUrl)
if (__DEBUG__) console.log('invalied session cookie, redirecting to login')
if (__DEBUG__) console.log(error)
})
})
app.options('*', cors(corsOptionsDelegate))
export default functions.https.onRequest(app)
@tegument
Copy link
Author

tegument commented Mar 15, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment