Use __Secure- cookie prefix (#1998)
This commit is contained in:
parent
54d3b11fbc
commit
e7eece744f
@ -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
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
71
lib/auth.js
71
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))
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user