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 { getAuth } from 'firebase/auth'
|
||||||
import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage'
|
import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage'
|
||||||
import {
|
import {
|
||||||
onAuthStateChanged,
|
onIdTokenChanged,
|
||||||
GoogleAuthProvider,
|
GoogleAuthProvider,
|
||||||
signInWithPopup,
|
signInWithPopup,
|
||||||
} from 'firebase/auth'
|
} from 'firebase/auth'
|
||||||
|
@ -43,6 +43,7 @@ import utc from 'dayjs/plugin/utc'
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
import { track } from '@amplitude/analytics-browser'
|
import { track } from '@amplitude/analytics-browser'
|
||||||
|
import { deleteAuthCookies, setAuthCookies } from './auth'
|
||||||
|
|
||||||
export const users = coll<User>('users')
|
export const users = coll<User>('users')
|
||||||
export const privateUsers = coll<PrivateUser>('private-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)
|
const cachedUser = local?.getItem(CACHED_USER_KEY)
|
||||||
onUser(cachedUser && JSON.parse(cachedUser))
|
onUser(cachedUser && JSON.parse(cachedUser))
|
||||||
|
|
||||||
return onAuthStateChanged(auth, async (fbUser) => {
|
return onIdTokenChanged(auth, async (fbUser) => {
|
||||||
if (fbUser) {
|
if (fbUser) {
|
||||||
let user: User | null = await getUser(fbUser.uid)
|
let user: User | null = await getUser(fbUser.uid)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
if (createUserPromise == null) {
|
if (createUserPromise == null) {
|
||||||
const local = safeLocalStorage()
|
const local = safeLocalStorage()
|
||||||
|
@ -204,17 +204,19 @@ export function listenForLogin(onUser: (user: User | null) => void) {
|
||||||
}
|
}
|
||||||
user = await createUserPromise
|
user = await createUserPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
onUser(user)
|
onUser(user)
|
||||||
|
|
||||||
// Persist to local storage, to reduce login blink next time.
|
// Persist to local storage, to reduce login blink next time.
|
||||||
// Note: Cap on localStorage size is ~5mb
|
// Note: Cap on localStorage size is ~5mb
|
||||||
local?.setItem(CACHED_USER_KEY, JSON.stringify(user))
|
local?.setItem(CACHED_USER_KEY, JSON.stringify(user))
|
||||||
setCachedReferralInfoForUser(user)
|
setCachedReferralInfoForUser(user)
|
||||||
|
setAuthCookies(await fbUser.getIdToken(), fbUser.refreshToken)
|
||||||
} else {
|
} else {
|
||||||
// User logged out; reset to null
|
// User logged out; reset to null
|
||||||
onUser(null)
|
onUser(null)
|
||||||
|
createUserPromise = undefined
|
||||||
local?.removeItem(CACHED_USER_KEY)
|
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