Skip to content

Instantly share code, notes, and snippets.

@gustavopch
Created September 21, 2023 12:14
Show Gist options
  • Save gustavopch/fc1a2428e76176cee83105ea252ac6af to your computer and use it in GitHub Desktop.
Save gustavopch/fc1a2428e76176cee83105ea252ac6af to your computer and use it in GitHub Desktop.
Remix + Firebase Auth
import { createRequestHandler } from '@remix-run/express'
import { broadcastDevReady } from '@remix-run/node'
import cookie from 'cookie'
import express from 'express'
import { getApp as getAdminApp } from 'firebase-admin/app'
import { getAuth as getAdminAuth } from 'firebase-admin/auth'
import { type FirebaseApp, deleteApp, initializeApp } from 'firebase/app'
import { getAuth, signInWithCustomToken } from 'firebase/auth'
import { LRUCache } from 'lru-cache'
import { env } from './app/env.js'
import { randomId } from './app/misc.js'
// Import the build dynamically just so TypeScript doesn't try to type-check it.
const build = import('./build/index.js' as string)
const LRU_MAX = 100
const LRU_TTL = 5 * 60 * 1000
const COOKIE_MAX_AGE = 5 * 24 * 60 * 60 * 1000
const ID_TOKEN_MAX_AGE = 5 * 60
const firebaseAppsLRU = new LRUCache<string, FirebaseApp>({
max: LRU_MAX,
ttl: LRU_TTL,
noDisposeOnSet: true,
updateAgeOnGet: true,
dispose: app => {
void deleteApp(app)
},
})
export const app = express()
app.use(express.static('public'))
app.all('*', async (request, response, next) => {
await build // Wait so we're sure Firebase is initialized
if (request.url === '/__session') {
await mintCookie(request, response)
} else {
const { user } = await handleAuth(request, response)
response.cookie(
'__FIREBASE_DEFAULTS__',
Buffer.from(env('__FIREBASE_DEFAULTS__')).toString('base64url'),
{ sameSite: 'strict' },
)
void createRequestHandler({
build: await build,
mode: process.env.NODE_ENV,
getLoadContext: () => ({ user }),
})(request, response, next)
}
})
if (process.argv[1] === import.meta.url.replace('file://', '')) {
const port = process.env.PORT || 3000
app.listen(port, async () => {
console.log(`\n 🌐 Listening at: http://localhost:${port}`)
if (process.env.NODE_ENV === 'development') {
broadcastDevReady(await build)
}
})
} else {
// The module was loaded by Firebase, so we know it's ready.
void build.then($ => broadcastDevReady($, 'http://localhost:3001'))
}
const mintCookie = async (
request: express.Request,
response: express.Response,
) => {
const adminAuth = getAdminAuth(getAdminApp())
const idToken = request.header('authorization')?.replace('Bearer ', '')
if (idToken) {
const decodedIdToken = await adminAuth.verifyIdToken(idToken)
if (Date.now() / 1000 - decodedIdToken.iat > ID_TOKEN_MAX_AGE) {
response.status(301).end()
} else {
const cookie = await adminAuth
.createSessionCookie(idToken, {
expiresIn: COOKIE_MAX_AGE,
})
.catch(error => console.error(error.message))
if (cookie) {
response
.cookie('__session', cookie, {
maxAge: COOKIE_MAX_AGE,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
})
.status(201)
.end()
} else {
response.status(401).end()
}
}
} else {
response.status(204).clearCookie('__session').end()
}
}
const handleAuth = async (
request: express.Request,
response: express.Response,
) => {
const adminAuth = getAdminAuth(getAdminApp())
const { __session } = cookie.parse(request.headers.cookie || '')
if (!__session) {
return { user: null }
}
const decodedIdToken = await adminAuth
.verifySessionCookie(__session)
.catch(error => console.error(error.message))
if (!decodedIdToken) {
return { user: null }
}
let app = firebaseAppsLRU.get(decodedIdToken.uid)
if (!app) {
const revoked = !(await adminAuth
.verifySessionCookie(__session, true)
.catch(error => console.error(error.message)))
if (revoked) {
return { user: null }
}
const appName = `authenticated-context:${decodedIdToken.uid}:${randomId({
size: 10,
})}`
// Passing undefined forces auto init with __FIREBASE_DEFAULTS__.
app = initializeApp(undefined as any, appName)
firebaseAppsLRU.set(decodedIdToken.uid, app)
}
const auth = getAuth(app)
if (auth.currentUser?.uid !== decodedIdToken.uid) {
const customToken = await adminAuth
.createCustomToken(decodedIdToken.uid)
.catch(error => console.error(error.message))
if (!customToken) {
return { user: null }
}
await signInWithCustomToken(auth, customToken)
}
return { user: auth.currentUser }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment