Created
September 21, 2023 12:14
-
-
Save gustavopch/fc1a2428e76176cee83105ea252ac6af to your computer and use it in GitHub Desktop.
Remix + Firebase Auth
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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