Skip to content

Commit

Permalink
Reset multi_auth cookies on error
Browse files Browse the repository at this point in the history
  • Loading branch information
ekzyis committed Mar 9, 2025
1 parent 6586870 commit dcbf39e
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 37 deletions.
77 changes: 55 additions & 22 deletions components/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,31 @@ import { useQuery } from '@apollo/client'
import { UserListRow } from '@/components/user-list'
import Link from 'next/link'
import AddIcon from '@/svgs/add-fill.svg'
import { MultiAuthErrorBanner } from './banners'

const AccountContext = createContext()

const CHECK_ERRORS_INTERVAL_MS = 5_000

const b64Decode = str => Buffer.from(str, 'base64').toString('utf-8')
const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')

const maybeSecureCookie = cookie => {
return window.location.protocol === 'https:' ? cookie + '; Secure' : cookie
}

export const AccountProvider = ({ children }) => {
const { me } = useMe()
const [accounts, setAccounts] = useState([])
const [meAnon, setMeAnon] = useState(true)
const [errors, setErrors] = useState([])

const updateAccountsFromCookie = useCallback(() => {
try {
const { multi_auth: multiAuthCookie } = cookie.parse(document.cookie)
const accounts = multiAuthCookie
? JSON.parse(b64Decode(multiAuthCookie))
: me ? [{ id: Number(me.id), name: me.name, photoId: me.photoId }] : []
setAccounts(accounts)
// required for backwards compatibility: sync cookie with accounts if no multi auth cookie exists
// this is the case for sessions that existed before we deployed account switching
if (!multiAuthCookie && !!me) {
document.cookie = maybeSecureCookie(`multi_auth=${b64Encode(accounts)}; Path=/`)
}
} catch (err) {
console.error('error parsing cookies:', err)
}
const { multi_auth: multiAuthCookie } = cookie.parse(document.cookie)
const accounts = multiAuthCookie
? JSON.parse(b64Decode(multiAuthCookie))
: []
setAccounts(accounts)
}, [])

useEffect(updateAccountsFromCookie, [])

const addAccount = useCallback(user => {
setAccounts(accounts => [...accounts, user])
}, [])
Expand All @@ -59,15 +50,43 @@ export const AccountProvider = ({ children }) => {
return switchSuccess
}, [updateAccountsFromCookie])

const checkErrors = useCallback(() => {
const {
multi_auth: multiAuthCookie,
'multi_auth.user-id': multiAuthUserIdCookie
} = 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')

setErrors(errors)
}, [])

useEffect(() => {
if (SSR) return

updateAccountsFromCookie()

const { 'multi_auth.user-id': multiAuthUserIdCookie } = cookie.parse(document.cookie)
setMeAnon(multiAuthUserIdCookie === 'anonymous')
}, [])

const interval = setInterval(checkErrors, CHECK_ERRORS_INTERVAL_MS)
return () => clearInterval(interval)
}, [updateAccountsFromCookie, checkErrors])

const value = useMemo(
() => ({ accounts, addAccount, removeAccount, meAnon, setMeAnon, multiAuthSignout }),
[accounts, addAccount, removeAccount, meAnon, setMeAnon, multiAuthSignout])
() => ({
accounts,
addAccount,
removeAccount,
meAnon,
setMeAnon,
multiAuthSignout,
multiAuthErrors: errors
}),
[accounts, addAccount, removeAccount, meAnon, setMeAnon, multiAuthSignout, errors])
return <AccountContext.Provider value={value}>{children}</AccountContext.Provider>
}

Expand Down Expand Up @@ -129,9 +148,23 @@ const AccountListRow = ({ account, ...props }) => {
}

export default function SwitchAccountList () {
const { accounts } = useAccounts()
const { accounts, multiAuthErrors } = useAccounts()
const router = useRouter()

const hasError = multiAuthErrors.length > 0

if (hasError) {
return (
<>
<div className='my-2'>
<div className='d-flex flex-column flex-wrap mt-2 mb-3'>
<MultiAuthErrorBanner errors={multiAuthErrors} />
</div>
</div>
</>
)
}

// can't show hat since the streak is not included in the JWT payload
return (
<>
Expand Down
15 changes: 15 additions & 0 deletions components/banners.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,18 @@ export function AuthBanner () {
</Alert>
)
}

export function MultiAuthErrorBanner ({ errors }) {
return (
<Alert className={`${styles.banner} mt-0`} key='info' variant='danger'>
<div className='fw-bold mb-3'>Account switching is currently unavailable</div>
We have detected the following issues:
<ul>
{errors.map((err, i) => (
<li key={i}>{err}</li>
))}
</ul>
To resolve these issues, please sign out and sign in again.
</Alert>
)
}
20 changes: 12 additions & 8 deletions components/nav/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,15 +314,19 @@ function LogoutObstacle ({ onClose }) {

export function LogoutDropdownItem ({ handleClose }) {
const showModal = useShowModal()
const { multiAuthError } = useAccounts()

return (
<>
<Dropdown.Item onClick={() => {
handleClose?.()
showModal(onClose => <SwitchAccountList onClose={onClose} />)
}}
>switch account
</Dropdown.Item>
{
!multiAuthError &&
<Dropdown.Item onClick={() => {
handleClose?.()
showModal(onClose => <SwitchAccountList onClose={onClose} />)
}}
>switch account
</Dropdown.Item>
}
<Dropdown.Item
onClick={async () => {
handleClose?.()
Expand All @@ -336,9 +340,9 @@ export function LogoutDropdownItem ({ handleClose }) {

function SwitchAccountButton ({ handleClose }) {
const showModal = useShowModal()
const { accounts } = useAccounts()
const { multiAuthError } = useAccounts()

if (accounts.length === 0) return null
if (multiAuthError) return null

return (
<Button
Expand Down
40 changes: 34 additions & 6 deletions pages/api/auth/[...nextauth].js
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,20 @@ function getCallbacks (req, res) {
token.sub = Number(token.id)
}

// add multi_auth cookie for user that just logged in
if (user && req && res) {
const secret = process.env.NEXTAUTH_SECRET
const jwt = await encodeJWT({ token, secret })
const me = await prisma.user.findUnique({ where: { id: token.id } })
setMultiAuthCookies(req, res, { ...me, jwt })
if (req && res) {
if (user) {
// add multi_auth cookie for user that just logged in
const secret = process.env.NEXTAUTH_SECRET
const jwt = await encodeJWT({ token, secret })
const me = await prisma.user.findUnique({ where: { id: token.id } })
setMultiAuthCookies(req, res, { ...me, jwt })
} else if (!req.cookies.multi_auth || !req.cookies['multi_auth.user-id']) {
// TODO: also check for missing user JWTs
// something is wrong, reset multi auth until next login
resetMultiAuthCookies(req, res)
} else {
// TODO: refresh cookies
}
}

return token
Expand Down Expand Up @@ -175,6 +183,26 @@ function setMultiAuthCookies (req, res, { id, jwt, name, photoId }) {
res.appendHeader('Set-Cookie', cookie.serialize('multi_auth', b64Encode(newMultiAuth), { ...cookieOptions, httpOnly: false }))
}

function resetMultiAuthCookies (req, res) {
const cookieOptions = {
path: '/',
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
expires: 0,
maxAge: 0
}

if (req.cookies.multi_auth) res.appendHeader('Set-Cookie', cookie.serialize('multi_auth', '', { ...cookieOptions, httpOnly: false }))
if (req.cookies['multi_auth.user-id']) res.appendHeader('Set-Cookie', cookie.serialize('multi_auth.user-id', '', { ...cookieOptions, httpOnly: false }))

for (const key of Object.keys(req.cookies)) {
// reset all user JWTs
if (/^multi_auth\.\d+$/.test(key)) {
res.appendHeader('Set-Cookie', cookie.serialize(key, '', { ...cookieOptions, httpOnly: true }))
}
}
}

async function pubkeyAuth (credentials, req, res, pubkeyColumnName) {
const { k1, pubkey } = credentials

Expand Down
2 changes: 1 addition & 1 deletion pages/api/graphql.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default startServerAndCreateNextHandler(apolloServer, {
}
} else {
req = multiAuthMiddleware(req)
session = await getServerSession(req, res, getAuthOptions(req))
session = await getServerSession(req, res, getAuthOptions(req, res))
}
return {
models,
Expand Down

0 comments on commit dcbf39e

Please sign in to comment.