Set a cookie with an up-to-date Firebase ID token
This commit is contained in:
parent
d1ad0716c8
commit
1576c2f12a
54
web/lib/firebase/auth.ts
Normal file
54
web/lib/firebase/auth.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { PROJECT_ID } from 'common/envs/constants'
|
||||
import { setCookie, getCookies } from '../util/cookie'
|
||||
import { IncomingMessage, ServerResponse } from 'http'
|
||||
|
||||
const TOKEN_KINDS = ['refresh', 'id'] as const
|
||||
type TokenKind = typeof TOKEN_KINDS[number]
|
||||
|
||||
const getAuthCookieName = (kind: TokenKind) => {
|
||||
const suffix = `${PROJECT_ID}_${kind}`.toUpperCase().replaceAll('-', '_')
|
||||
return `FIREBASE_TOKEN_${suffix}`
|
||||
}
|
||||
|
||||
const ID_COOKIE_NAME = getAuthCookieName('id')
|
||||
const REFRESH_COOKIE_NAME = getAuthCookieName('refresh')
|
||||
|
||||
export const getAuthCookies = (request?: IncomingMessage) => {
|
||||
const data = request != null ? request.headers.cookie ?? '' : document.cookie
|
||||
const cookies = getCookies(data)
|
||||
return {
|
||||
idToken: cookies[ID_COOKIE_NAME] as string | undefined,
|
||||
refreshToken: cookies[REFRESH_COOKIE_NAME] as string | undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export const setAuthCookies = (
|
||||
idToken?: string,
|
||||
refreshToken?: string,
|
||||
response?: ServerResponse
|
||||
) => {
|
||||
// these tokens last an hour
|
||||
const idMaxAge = idToken != null ? 60 * 60 : 0
|
||||
const idCookie = setCookie(ID_COOKIE_NAME, idToken ?? '', [
|
||||
['path', '/'],
|
||||
['max-age', idMaxAge.toString()],
|
||||
['samesite', 'lax'],
|
||||
['secure'],
|
||||
])
|
||||
// these tokens don't expire
|
||||
const refreshMaxAge = refreshToken != null ? 60 * 60 * 24 * 365 * 10 : 0
|
||||
const refreshCookie = setCookie(REFRESH_COOKIE_NAME, refreshToken ?? '', [
|
||||
['path', '/'],
|
||||
['max-age', refreshMaxAge.toString()],
|
||||
['samesite', 'lax'],
|
||||
['secure'],
|
||||
])
|
||||
if (response != null) {
|
||||
response.setHeader('Set-Cookie', [idCookie, refreshCookie])
|
||||
} else {
|
||||
document.cookie = idCookie
|
||||
document.cookie = refreshCookie
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteAuthCookies = () => setAuthCookies()
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { getAuth } from 'firebase/auth'
|
||||
import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage'
|
||||
import {
|
||||
onAuthStateChanged,
|
||||
onIdTokenChanged,
|
||||
GoogleAuthProvider,
|
||||
signInWithPopup,
|
||||
} from 'firebase/auth'
|
||||
|
@ -43,6 +43,7 @@ import utc from 'dayjs/plugin/utc'
|
|||
dayjs.extend(utc)
|
||||
|
||||
import { track } from '@amplitude/analytics-browser'
|
||||
import { deleteAuthCookies, setAuthCookies } from './auth'
|
||||
|
||||
export const users = coll<User>('users')
|
||||
export const privateUsers = coll<PrivateUser>('private-users')
|
||||
|
@ -188,10 +189,9 @@ export function listenForLogin(onUser: (user: User | null) => void) {
|
|||
const cachedUser = local?.getItem(CACHED_USER_KEY)
|
||||
onUser(cachedUser && JSON.parse(cachedUser))
|
||||
|
||||
return onAuthStateChanged(auth, async (fbUser) => {
|
||||
return onIdTokenChanged(auth, async (fbUser) => {
|
||||
if (fbUser) {
|
||||
let user: User | null = await getUser(fbUser.uid)
|
||||
|
||||
if (!user) {
|
||||
if (createUserPromise == null) {
|
||||
const local = safeLocalStorage()
|
||||
|
@ -204,17 +204,19 @@ export function listenForLogin(onUser: (user: User | null) => void) {
|
|||
}
|
||||
user = await createUserPromise
|
||||
}
|
||||
|
||||
onUser(user)
|
||||
|
||||
// Persist to local storage, to reduce login blink next time.
|
||||
// Note: Cap on localStorage size is ~5mb
|
||||
local?.setItem(CACHED_USER_KEY, JSON.stringify(user))
|
||||
setCachedReferralInfoForUser(user)
|
||||
setAuthCookies(await fbUser.getIdToken(), fbUser.refreshToken)
|
||||
} else {
|
||||
// User logged out; reset to null
|
||||
onUser(null)
|
||||
createUserPromise = undefined
|
||||
local?.removeItem(CACHED_USER_KEY)
|
||||
deleteAuthCookies()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
26
web/lib/util/cookie.ts
Normal file
26
web/lib/util/cookie.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
type CookieOptions = string[][]
|
||||
|
||||
const encodeCookie = (name: string, val: string) => {
|
||||
return `${name}=${encodeURIComponent(val)}`
|
||||
}
|
||||
|
||||
const decodeCookie = (cookie: string) => {
|
||||
const parts = cookie.trim().split('=')
|
||||
if (parts.length != 2) {
|
||||
throw new Error(`Invalid cookie contents: ${cookie}`)
|
||||
}
|
||||
return [parts[0], decodeURIComponent(parts[1])] as const
|
||||
}
|
||||
|
||||
export const setCookie = (name: string, val: string, opts?: CookieOptions) => {
|
||||
const parts = [encodeCookie(name, val)]
|
||||
if (opts != null) {
|
||||
parts.push(...opts.map((opt) => opt.join('=')))
|
||||
}
|
||||
return parts.join('; ')
|
||||
}
|
||||
|
||||
// Note that this intentionally ignores the case where multiple cookies have
|
||||
// the same name but different paths. Hopefully we never need to think about it.
|
||||
export const getCookies = (cookies: string) =>
|
||||
Object.fromEntries(cookies.split(';').map(decodeCookie))
|
Loading…
Reference in New Issue
Block a user