From e7eece744fabd15d13f72984f9125ca1b41dfddf Mon Sep 17 00:00:00 2001 From: ekzyis Date: Sat, 22 Mar 2025 16:59:57 -0500 Subject: [PATCH] Use __Secure- cookie prefix (#1998) --- api/ssrApollo.js | 3 +- components/account.js | 22 ++++++------ lib/auth.js | 71 +++++++++++++++++++++------------------ pages/api/next-account.js | 27 +++++++-------- pages/login.js | 3 +- 5 files changed, 66 insertions(+), 60 deletions(-) diff --git a/api/ssrApollo.js b/api/ssrApollo.js index 7af73317..d5513b7d 100644 --- a/api/ssrApollo.js +++ b/api/ssrApollo.js @@ -15,6 +15,7 @@ import { getServerSession } from 'next-auth/next' import { getAuthOptions } from '@/pages/api/auth/[...nextauth]' import { NOFOLLOW_LIMIT } from '@/lib/constants' import { satsToMsats } from '@/lib/format' +import { MULTI_AUTH_ANON, MULTI_AUTH_LIST } from '@/lib/auth' export default async function getSSRApolloClient ({ req, res, me = null }) { const session = req && await getServerSession(req, res, getAuthOptions(req)) @@ -155,7 +156,7 @@ export function getGetServerSideProps ( // required to redirect to /signup on page reload // if we switched to anon and authentication is required - if (req.cookies['multi_auth.user-id'] === 'anonymous') { + if (req.cookies[MULTI_AUTH_LIST] === MULTI_AUTH_ANON) { me = null } diff --git a/components/account.js b/components/account.js index f74c1b86..18394b20 100644 --- a/components/account.js +++ b/components/account.js @@ -9,7 +9,7 @@ import { UserListRow } from '@/components/user-list' import Link from 'next/link' import AddIcon from '@/svgs/add-fill.svg' import { MultiAuthErrorBanner } from '@/components/banners' -import { cookieOptions } from '@/lib/auth' +import { cookieOptions, MULTI_AUTH_ANON, MULTI_AUTH_LIST, MULTI_AUTH_POINTER } from '@/lib/auth' const AccountContext = createContext() @@ -23,9 +23,9 @@ export const AccountProvider = ({ children }) => { const [errors, setErrors] = useState([]) const updateAccountsFromCookie = useCallback(() => { - const { multi_auth: multiAuthCookie } = cookie.parse(document.cookie) - const accounts = multiAuthCookie - ? JSON.parse(b64Decode(multiAuthCookie)) + const { [MULTI_AUTH_LIST]: listCookie } = cookie.parse(document.cookie) + const accounts = listCookie + ? JSON.parse(b64Decode(listCookie)) : [] setAccounts(accounts) }, []) @@ -49,14 +49,14 @@ export const AccountProvider = ({ children }) => { const checkErrors = useCallback(() => { const { - multi_auth: multiAuthCookie, - 'multi_auth.user-id': multiAuthUserIdCookie + [MULTI_AUTH_LIST]: listCookie, + [MULTI_AUTH_POINTER]: pointerCookie } = cookie.parse(document.cookie) const errors = [] - if (!multiAuthCookie) errors.push('multi_auth cookie not found') - if (!multiAuthUserIdCookie) errors.push('multi_auth.user-id cookie not found') + if (!listCookie) errors.push(`${MULTI_AUTH_LIST} cookie not found`) + if (!pointerCookie) errors.push(`${MULTI_AUTH_POINTER} cookie not found`) setErrors(errors) }, []) @@ -66,8 +66,8 @@ export const AccountProvider = ({ children }) => { updateAccountsFromCookie() - const { 'multi_auth.user-id': multiAuthUserIdCookie } = cookie.parse(document.cookie) - setMeAnon(multiAuthUserIdCookie === 'anonymous') + const { [MULTI_AUTH_POINTER]: pointerCookie } = cookie.parse(document.cookie) + setMeAnon(pointerCookie === 'anonymous') const interval = setInterval(checkErrors, CHECK_ERRORS_INTERVAL_MS) return () => clearInterval(interval) @@ -113,7 +113,7 @@ const AccountListRow = ({ account, ...props }) => { // update pointer cookie const options = cookieOptions({ httpOnly: false }) - document.cookie = cookie.serialize('multi_auth.user-id', anonRow ? 'anonymous' : account.id, options) + document.cookie = cookie.serialize(MULTI_AUTH_POINTER, anonRow ? MULTI_AUTH_ANON : account.id, options) // update state if (anonRow) { diff --git a/lib/auth.js b/lib/auth.js index 21ebe84f..de1384b6 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -6,14 +6,26 @@ import { encode as encodeJWT, decode as decodeJWT } from 'next-auth/jwt' const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64') const b64Decode = s => JSON.parse(Buffer.from(s, 'base64')) -const userJwtRegexp = /^multi_auth\.\d+$/ +export const HTTPS = process.env.NODE_ENV === 'production' -const HTTPS = process.env.NODE_ENV === 'production' -const SESSION_COOKIE_NAME = HTTPS ? '__Secure-next-auth.session-token' : 'next-auth.session-token' +const secureCookie = (name) => + HTTPS + ? `__Secure-${name}` + : name + +export const SESSION_COOKIE = secureCookie('next-auth.session-token') +export const MULTI_AUTH_LIST = secureCookie('multi_auth') +export const MULTI_AUTH_POINTER = secureCookie('multi_auth.user-id') +export const MULTI_AUTH_ANON = 'anonymous' + +export const MULTI_AUTH_JWT = id => secureCookie(`multi_auth.${id}`) + +const MULTI_AUTH_REGEXP = /^(__Secure-)?multi_auth/ +const MULTI_AUTH_JWT_REGEXP = /^(__Secure-)?multi_auth\.\d+$/ export const cookieOptions = (args) => ({ path: '/', - secure: process.env.NODE_ENV === 'production', + secure: HTTPS, // httpOnly cookies by default httpOnly: true, sameSite: 'lax', @@ -28,44 +40,43 @@ export function setMultiAuthCookies (req, res, { id, jwt, name, photoId }) { const jsOptions = { ...httpOnlyOptions, httpOnly: false } // add JWT to **httpOnly** cookie - res.appendHeader('Set-Cookie', cookie.serialize(`multi_auth.${id}`, jwt, httpOnlyOptions)) + res.appendHeader('Set-Cookie', cookie.serialize(MULTI_AUTH_JWT(id), jwt, httpOnlyOptions)) // switch to user we just added - res.appendHeader('Set-Cookie', cookie.serialize('multi_auth.user-id', id, jsOptions)) + res.appendHeader('Set-Cookie', cookie.serialize(MULTI_AUTH_POINTER, id, jsOptions)) let newMultiAuth = [{ id, name, photoId }] - if (req.cookies.multi_auth) { - const oldMultiAuth = b64Decode(req.cookies.multi_auth) + if (req.cookies[MULTI_AUTH_LIST]) { + const oldMultiAuth = b64Decode(req.cookies[MULTI_AUTH_LIST]) // make sure we don't add duplicates if (oldMultiAuth.some(({ id: id_ }) => id_ === id)) return newMultiAuth = [...oldMultiAuth, ...newMultiAuth] } - res.appendHeader('Set-Cookie', cookie.serialize('multi_auth', b64Encode(newMultiAuth), jsOptions)) + res.appendHeader('Set-Cookie', cookie.serialize(MULTI_AUTH_LIST, b64Encode(newMultiAuth), jsOptions)) } function switchSessionCookie (request) { // switch next-auth session cookie with multi_auth cookie if cookie pointer present // is there a cookie pointer? - const cookiePointerName = 'multi_auth.user-id' - const hasCookiePointer = !!request.cookies[cookiePointerName] + const hasCookiePointer = !!request.cookies[MULTI_AUTH_POINTER] // is there a session? - const hasSession = !!request.cookies[SESSION_COOKIE_NAME] + const hasSession = !!request.cookies[SESSION_COOKIE] if (!hasCookiePointer || !hasSession) { // no session or no cookie pointer. do nothing. return request } - const userId = request.cookies[cookiePointerName] - if (userId === 'anonymous') { + const userId = request.cookies[MULTI_AUTH_POINTER] + if (userId === MULTI_AUTH_ANON) { // user switched to anon. only delete session cookie. - delete request.cookies[SESSION_COOKIE_NAME] + delete request.cookies[SESSION_COOKIE] return request } - const userJWT = request.cookies[`multi_auth.${userId}`] + const userJWT = request.cookies[MULTI_AUTH_JWT(userId)] if (!userJWT) { // no JWT for account switching found return request @@ -73,21 +84,21 @@ function switchSessionCookie (request) { if (userJWT) { // use JWT found in cookie pointed to by cookie pointer - request.cookies[SESSION_COOKIE_NAME] = userJWT + request.cookies[SESSION_COOKIE] = userJWT return request } return request } -function checkMultiAuthCookies (req, res) { - if (!req.cookies.multi_auth || !req.cookies['multi_auth.user-id']) { +export function checkMultiAuthCookies (req, res) { + if (!req.cookies[MULTI_AUTH_LIST] || !req.cookies[MULTI_AUTH_POINTER]) { return false } - const accounts = b64Decode(req.cookies.multi_auth) + const accounts = b64Decode(req.cookies[MULTI_AUTH_LIST]) for (const account of accounts) { - if (!req.cookies[`multi_auth.${account.id}`]) { + if (!req.cookies[MULTI_AUTH_JWT(account.id)]) { return false } } @@ -99,14 +110,10 @@ function resetMultiAuthCookies (req, res) { const httpOnlyOptions = cookieOptions({ expires: 0, maxAge: 0 }) const jsOptions = { ...httpOnlyOptions, httpOnly: false } - if ('multi_auth' in req.cookies) res.appendHeader('Set-Cookie', cookie.serialize('multi_auth', '', jsOptions)) - if ('multi_auth.user-id' in req.cookies) res.appendHeader('Set-Cookie', cookie.serialize('multi_auth.user-id', '', jsOptions)) - for (const key of Object.keys(req.cookies)) { - // reset all user JWTs - if (userJwtRegexp.test(key)) { - res.appendHeader('Set-Cookie', cookie.serialize(key, '', httpOnlyOptions)) - } + if (!MULTI_AUTH_REGEXP.test(key)) continue + const options = MULTI_AUTH_JWT_REGEXP.test(key) ? httpOnlyOptions : jsOptions + res.appendHeader('Set-Cookie', cookie.serialize(key, '', options)) } } @@ -126,15 +133,15 @@ async function refreshMultiAuthCookies (req, res) { }) } - const isAnon = req.cookies['multi_auth.user-id'] === 'anonymous' + const isAnon = req.cookies[MULTI_AUTH_POINTER] === MULTI_AUTH_ANON for (const [key, value] of Object.entries(req.cookies)) { // only refresh session cookie manually if we switched to anon since else it's already handled by next-auth - if (key === SESSION_COOKIE_NAME && !isAnon) continue + if (key === SESSION_COOKIE && !isAnon) continue - if (!key.startsWith('multi_auth') && key !== SESSION_COOKIE_NAME) continue + if (!key.startsWith(MULTI_AUTH_LIST) && key !== SESSION_COOKIE) continue - if (userJwtRegexp.test(key) || key === SESSION_COOKIE_NAME) { + if (MULTI_AUTH_JWT_REGEXP.test(key) || key === SESSION_COOKIE) { const oldToken = value const newToken = await refreshToken(oldToken) res.appendHeader('Set-Cookie', cookie.serialize(key, newToken, httpOnlyOptions)) diff --git a/pages/api/next-account.js b/pages/api/next-account.js index cc9d1421..cfabf462 100644 --- a/pages/api/next-account.js +++ b/pages/api/next-account.js @@ -1,5 +1,6 @@ import * as cookie from 'cookie' import { datePivot } from '@/lib/time' +import { HTTPS, MULTI_AUTH_JWT, MULTI_AUTH_LIST, MULTI_AUTH_POINTER, SESSION_COOKIE } from '@/lib/auth' /** * @param {NextApiRequest} req @@ -8,14 +9,10 @@ import { datePivot } from '@/lib/time' */ export default (req, res) => { // is there a cookie pointer? - const cookiePointerName = 'multi_auth.user-id' - const userId = req.cookies[cookiePointerName] - - const secure = process.env.NODE_ENV === 'production' + const userId = req.cookies[MULTI_AUTH_POINTER] // is there a session? - const sessionCookieName = secure ? '__Secure-next-auth.session-token' : 'next-auth.session-token' - const sessionJWT = req.cookies[sessionCookieName] + const sessionJWT = req.cookies[SESSION_COOKIE] if (!userId && !sessionJWT) { // no cookie pointer and no session cookie present. nothing to do. @@ -27,33 +24,33 @@ export default (req, res) => { const cookieOptions = { path: '/', - secure, + secure: HTTPS, httpOnly: true, sameSite: 'lax', expires: datePivot(new Date(), { months: 1 }) } // remove JWT pointed to by cookie pointer - cookies.push(cookie.serialize(`multi_auth.${userId}`, '', { ...cookieOptions, expires: 0, maxAge: 0 })) + cookies.push(cookie.serialize(MULTI_AUTH_JWT(userId), '', { ...cookieOptions, expires: 0, maxAge: 0 })) // update multi_auth cookie and check if there are more accounts available - const oldMultiAuth = req.cookies.multi_auth ? b64Decode(req.cookies.multi_auth) : undefined + const oldMultiAuth = req.cookies[MULTI_AUTH_LIST] ? b64Decode(req.cookies[MULTI_AUTH_LIST]) : undefined const newMultiAuth = oldMultiAuth?.filter(({ id }) => id !== Number(userId)) if (!oldMultiAuth || newMultiAuth?.length === 0) { // no next account available. cleanup: remove multi_auth + pointer cookie - cookies.push(cookie.serialize('multi_auth', '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 })) - cookies.push(cookie.serialize('multi_auth.user-id', '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 })) + cookies.push(cookie.serialize(MULTI_AUTH_LIST, '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 })) + cookies.push(cookie.serialize(MULTI_AUTH_POINTER, '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 })) res.setHeader('Set-Cookie', cookies) res.status(204).end() return } - cookies.push(cookie.serialize('multi_auth', b64Encode(newMultiAuth), { ...cookieOptions, httpOnly: false })) + cookies.push(cookie.serialize(MULTI_AUTH_LIST, b64Encode(newMultiAuth), { ...cookieOptions, httpOnly: false })) const newUserId = newMultiAuth[0].id - const newUserJWT = req.cookies[`multi_auth.${newUserId}`] + const newUserJWT = req.cookies[MULTI_AUTH_JWT(newUserId)] res.setHeader('Set-Cookie', [ ...cookies, - cookie.serialize(cookiePointerName, newUserId, { ...cookieOptions, httpOnly: false }), - cookie.serialize(sessionCookieName, newUserJWT, cookieOptions) + cookie.serialize(MULTI_AUTH_POINTER, newUserId, { ...cookieOptions, httpOnly: false }), + cookie.serialize(SESSION_COOKIE, newUserJWT, cookieOptions) ]) res.status(302).end() diff --git a/pages/login.js b/pages/login.js index 4c56ef67..b31df60e 100644 --- a/pages/login.js +++ b/pages/login.js @@ -5,6 +5,7 @@ import Link from 'next/link' import { StaticLayout } from '@/components/layout' import Login from '@/components/login' import { isExternal } from '@/lib/url' +import { MULTI_AUTH_ANON, MULTI_AUTH_POINTER } from '@/lib/auth' export async function getServerSideProps ({ req, res, query: { callbackUrl, multiAuth = false, error = null } }) { let session = await getServerSession(req, res, getAuthOptions(req)) @@ -12,7 +13,7 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, mult // required to prevent infinite redirect loops if we switch to anon // but are on a page that would redirect us to /signup. // without this code, /signup would redirect us back to the callbackUrl. - if (req.cookies['multi_auth.user-id'] === 'anonymous') { + if (req.cookies[MULTI_AUTH_POINTER] === MULTI_AUTH_ANON) { session = null }