manifold/web/lib/firebase/server-auth.ts
2022-08-31 22:13:26 -07:00

130 lines
4.7 KiB
TypeScript

import { IncomingMessage, ServerResponse } from 'http'
import { Auth as FirebaseAuth, User as FirebaseUser } from 'firebase/auth'
import { AUTH_COOKIE_NAME } from 'common/envs/constants'
import { getCookies } from 'web/lib/util/cookie'
import {
GetServerSideProps,
GetServerSidePropsContext,
GetServerSidePropsResult,
} from 'next'
// client firebase SDK
import { app as clientApp } from './init'
import { getAuth, updateCurrentUser } from 'firebase/auth'
type RequestContext = {
req: IncomingMessage
res: ServerResponse
}
// The Firebase SDK doesn't really support persisting the logged-in state between
// devices, or anything like that. To get it from the client to the server:
//
// 1. We pack up the user by calling (the undocumented) User.toJSON(). This is the
// same way the Firebase SDK saves it to disk, so it's gonna have the right stuff.
//
// 2. We put it into a cookie and read the cookie out here.
//
// 3. We use the Firebase "persistence manager" to write the cookie value into the persistent
// store on the server (an in-memory store), just as if the SDK had saved the user itself.
//
// 4. We ask the persistence manager for the current user, which reads what we just wrote,
// and creates a real puffed-up internal user object from the serialized user.
//
// 5. We set that user to be the current Firebase user in the SDK.
//
// 6. We ask for the ID token, which will refresh it if necessary (i.e. if this cookie
// is from an old browser session), so that we know the SDK is prepared to do real
// Firebase queries.
//
// This strategy should be robust, since it's repurposing Firebase's internal persistence
// machinery, but the details may eventually need updating for new versions of the SDK.
//
// References:
// Persistence manager: https://github.com/firebase/firebase-js-sdk/blob/39f4635ebc07316661324145f1b8c27f9bd7aedb/packages/auth/src/core/persistence/persistence_user_manager.ts#L64
// Token manager: https://github.com/firebase/firebase-js-sdk/blob/39f4635ebc07316661324145f1b8c27f9bd7aedb/packages/auth/src/core/user/token_manager.ts#L76
interface FirebaseAuthInternal extends FirebaseAuth {
persistenceManager: {
fullUserKey: string
getCurrentUser: () => Promise<FirebaseUser | null>
persistence: {
_set: (k: string, obj: Record<string, unknown>) => Promise<void>
}
}
}
export const authenticateOnServer = async (ctx: RequestContext) => {
const user = getCookies(ctx.req.headers.cookie ?? '')[AUTH_COOKIE_NAME]
if (user == null) {
console.debug('User is unauthenticated.')
return null
}
try {
const deserializedUser = JSON.parse(user)
const clientAuth = getAuth(clientApp) as FirebaseAuthInternal
const persistenceManager = clientAuth.persistenceManager
const persistence = persistenceManager.persistence
await persistence._set(persistenceManager.fullUserKey, deserializedUser)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const fbUser = (await persistenceManager.getCurrentUser())!
await fbUser.getIdToken() // forces a refresh if necessary
await updateCurrentUser(clientAuth, fbUser)
console.debug('Signed in with user from cookie.')
return fbUser
} catch (e) {
console.error(e)
return null
}
}
// note that we might want to define these types more generically if we want better
// type safety on next.js stuff... see the definition of GetServerSideProps
type GetServerSidePropsAuthed<P> = (
context: GetServerSidePropsContext,
creds: FirebaseUser
) => Promise<GetServerSidePropsResult<P>>
export const redirectIfLoggedIn = <P extends { [k: string]: any }>(
dest: string,
fn?: GetServerSideProps<P>
) => {
return async (ctx: GetServerSidePropsContext) => {
const creds = await authenticateOnServer(ctx)
if (creds == null) {
if (fn == null) {
return { props: {} }
} else {
const props = await fn(ctx)
console.debug('Finished getting initial props for rendering.')
return props
}
} else {
console.debug(`Redirecting to ${dest}.`)
return { redirect: { destination: dest, permanent: false } }
}
}
}
export const redirectIfLoggedOut = <P extends { [k: string]: any }>(
dest: string,
fn?: GetServerSidePropsAuthed<P>
) => {
return async (ctx: GetServerSidePropsContext) => {
const creds = await authenticateOnServer(ctx)
if (creds == null) {
console.debug(`Redirecting to ${dest}.`)
return { redirect: { destination: dest, permanent: false } }
} else {
if (fn == null) {
return { props: {} }
} else {
const props = await fn(ctx, creds)
console.debug('Finished getting initial props for rendering.')
return props
}
}
}
}