manifold/web/lib/firebase/server-auth.ts

94 lines
3.2 KiB
TypeScript

import * as admin from 'firebase-admin'
import fetch from 'node-fetch'
import { IncomingMessage, ServerResponse } from 'http'
import { FIREBASE_CONFIG, PROJECT_ID } from 'common/envs/constants'
import { getAuthCookies, setAuthCookies } from './auth'
import { GetServerSideProps, GetServerSidePropsContext } from 'next'
const ensureApp = async () => {
// Note: firebase-admin can only be imported from a server context,
// because it relies on Node standard library dependencies.
if (admin.apps.length === 0) {
// never initialize twice
return admin.initializeApp({ projectId: PROJECT_ID })
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return admin.apps[0]!
}
const requestFirebaseIdToken = async (refreshToken: string) => {
// See https://firebase.google.com/docs/reference/rest/auth/#section-refresh-token
const refreshUrl = new URL('https://securetoken.googleapis.com/v1/token')
refreshUrl.searchParams.append('key', FIREBASE_CONFIG.apiKey)
const result = await fetch(refreshUrl.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
}),
})
if (!result.ok) {
throw new Error(`Could not refresh ID token: ${await result.text()}`)
}
return (await result.json()) as any
}
type RequestContext = {
req: IncomingMessage
res: ServerResponse
}
export const getServerAuthenticatedUid = async (ctx: RequestContext) => {
const app = await ensureApp()
const auth = app.auth()
const { idToken, refreshToken } = getAuthCookies(ctx.req)
// If we have a valid ID token, verify the user immediately with no network trips.
// If the ID token doesn't verify, we'll have to refresh it to see who they are.
// If they don't have any tokens, then we have no idea who they are.
if (idToken != null) {
try {
return (await auth.verifyIdToken(idToken))?.uid
} catch {
// plausibly expired; try the refresh token, if it's present
}
}
if (refreshToken != null) {
try {
const resp = await requestFirebaseIdToken(refreshToken)
setAuthCookies(resp.id_token, resp.refresh_token, ctx.res)
return (await auth.verifyIdToken(resp.id_token))?.uid
} catch (e) {
// this is a big unexpected problem -- either their cookies are corrupt
// or the refresh token API is down. functionally, they are not logged in
console.error(e)
}
}
return undefined
}
export const redirectIfLoggedIn = (dest: string, fn?: GetServerSideProps) => {
return async (ctx: GetServerSidePropsContext) => {
const uid = await getServerAuthenticatedUid(ctx)
if (uid == null) {
return fn != null ? await fn(ctx) : { props: {} }
} else {
return { redirect: { destination: dest, permanent: false } }
}
}
}
export const redirectIfLoggedOut = (dest: string, fn?: GetServerSideProps) => {
return async (ctx: GetServerSidePropsContext) => {
const uid = await getServerAuthenticatedUid(ctx)
if (uid == null) {
return { redirect: { destination: dest, permanent: false } }
} else {
return fn != null ? await fn(ctx) : { props: {} }
}
}
}