Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: forward set-cookie header for useUserSession().clear() #282

Merged
merged 2 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion playground/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,14 @@ declare module '#auth-utils' {
interface UserSession {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
extended?: any
jtw?: {
accessToken: string
refreshToken: string
}
loggedInAt: number
secure?: Record<string, unknown>
}

interface SecureSessionData {
}
}

Expand Down
63 changes: 63 additions & 0 deletions playground/middleware/jtw.global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { appendResponseHeader } from 'h3'
import { parse, parseSetCookie, serialize } from 'cookie-es'
import type { JwtData } from '@tsndr/cloudflare-worker-jwt'
import { decode } from '@tsndr/cloudflare-worker-jwt'

export default defineNuxtRouteMiddleware(async () => {
const nuxtApp = useNuxtApp()
// Don't run on client hydration when server rendered
if (import.meta.client && nuxtApp.isHydrating && nuxtApp.payload.serverRendered) return

const { session, clear: clearSession, fetch: fetchSession } = useUserSession()
// Ignore if no tokens
if (!session.value?.jwt) return

const serverEvent = useRequestEvent()
const runtimeConfig = useRuntimeConfig()
const { accessToken, refreshToken } = session.value.jwt

const accessPayload = decode(accessToken)
const refreshPayload = decode(refreshToken)

// Both tokens expired, clearing session
if (isExpired(accessPayload) && isExpired(refreshPayload)) {
console.info('both tokens expired, clearing session')
await clearSession()
// return navigateTo('/login')
}
// Access token expired, refreshing
else if (isExpired(accessPayload)) {
console.info('access token expired, refreshing')
await useRequestFetch()('/api/jtw/refresh', {
method: 'POST',
onResponse({ response: { headers } }) {
// Forward the Set-Cookie header to the main server event
if (import.meta.server && serverEvent) {
for (const setCookie of headers.getSetCookie()) {
appendResponseHeader(serverEvent, 'Set-Cookie', setCookie)
// Update session cookie for next fetch requests
const { name, value } = parseSetCookie(setCookie)
if (name === runtimeConfig.session.name) {
// console.log('updating headers.cookie to', value)
const cookies = parse(serverEvent.headers.get('cookie') || '')
// set or overwrite existing cookie
cookies[name] = value
// update cookie event header for future requests
serverEvent.headers.set('cookie', Object.entries(cookies).map(([name, value]) => serialize(name, value)).join('; '))
// Also apply to serverEvent.node.req.headers
if (serverEvent.node?.req?.headers) {
serverEvent.node.req.headers['cookie'] = serverEvent.headers.get('cookie') || ''
}
}
}
}
},
})
// refresh the session
await fetchSession()
}
})

function isExpired(payload: JwtData) {
return payload.payload?.exp && payload.payload.exp < (Date.now() / 1000)
}
1 change: 1 addition & 0 deletions playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"dependencies": {
"@iconify-json/gravity-ui": "^1.2.2",
"@iconify-json/iconoir": "^1.2.3",
"@tsndr/cloudflare-worker-jwt": "^3.1.3",
"nuxt": "^3.14.159",
"nuxt-auth-utils": "latest",
"zod": "^3.23.8"
Expand Down
12 changes: 12 additions & 0 deletions playground/pages/about.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<UPageBody>
<h1>About page</h1>
<UButton
to="/"
variant="link"
:padded="false"
>
Home page
</UButton>
</UPageBody>
</template>
26 changes: 18 additions & 8 deletions playground/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,24 @@
...
</template>
</AuthState>
<UButton
to="/secret"
class="mt-2"
variant="link"
:padded="false"
>
Secret page
</UButton>
<div class="flex flex-col gap-2 mt-4">
<UButton
to="/secret"
class="mt-2"
variant="link"
:padded="false"
>
Secret page
</UButton>
<UButton
to="/about"
class="mt-2"
variant="link"
:padded="false"
>
About page
</UButton>
</div>
</UPageBody>
</UPage>
</template>
49 changes: 49 additions & 0 deletions playground/server/api/jtw/create.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import jwt from '@tsndr/cloudflare-worker-jwt'

export default defineEventHandler(async (event) => {
// Get user from session
const user = await getUserSession(event)
if (!user) {
throw createError({
statusCode: 401,
message: 'Unauthorized',
})
}

if (!process.env.NUXT_SESSION_PASSWORD) {
throw createError({
statusCode: 500,
message: 'Session secret not configured',
})
}

// Generate tokens
const accessToken = await jwt.sign(
{
hello: 'world',
exp: Math.floor(Date.now() / 1000) + 5, // 30 seconds
},
process.env.NUXT_SESSION_PASSWORD,
)

const refreshToken = await jwt.sign(
{
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, // 7 days
},
`${process.env.NUXT_SESSION_PASSWORD}-secret`,
)

await setUserSession(event, {
jwt: {
accessToken,
refreshToken,
},
loggedInAt: Date.now(),
})

// Return tokens
return {
accessToken,
refreshToken,
}
})
23 changes: 23 additions & 0 deletions playground/server/api/jtw/payload.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import jwt from '@tsndr/cloudflare-worker-jwt'

export default eventHandler(async (event) => {
const session = await getUserSession(event)
if (!session.jwt?.accessToken) {
throw createError({
statusCode: 401,
message: 'Unauthorized',
})
}

try {
return await jwt.verify(session.jwt.accessToken, process.env.NUXT_SESSION_PASSWORD!, {
throwError: true,
})
}
catch (err) {
throw createError({
statusCode: 401,
message: (err as Error).message,
})
}
})
39 changes: 39 additions & 0 deletions playground/server/api/jtw/refresh.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import jwt from '@tsndr/cloudflare-worker-jwt'

export default eventHandler(async (event) => {
const session = await getUserSession(event)
if (!session.jwt?.accessToken && !session.jwt?.refreshToken) {
throw createError({
statusCode: 401,
message: 'Unauthorized',
})
}

if (!await jwt.verify(session.jwt.refreshToken, `${process.env.NUXT_SESSION_PASSWORD!}-secret`)) {
throw createError({
statusCode: 401,
message: 'refresh token is invalid',
})
}

const accessToken = await jwt.sign(
{
hello: 'world',
exp: Math.floor(Date.now() / 1000) + 30, // 30 seconds
},
process.env.NUXT_SESSION_PASSWORD!,
)

await setUserSession(event, {
jwt: {
accessToken,
refreshToken: session.jwt.refreshToken,
},
loggedInAt: Date.now(),
})

return {
accessToken,
refreshToken: session.jwt.refreshToken,
}
})
Loading