Skip to content

Commit

Permalink
feat: forward set-cookie header for useUserSession().clear() (#282)
Browse files Browse the repository at this point in the history
* feat: forward set-cookie header for `useUserSession().clear()`

* chore: update
  • Loading branch information
atinux authored Nov 14, 2024
1 parent 5d58645 commit f147d1a
Show file tree
Hide file tree
Showing 10 changed files with 547 additions and 80 deletions.
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

0 comments on commit f147d1a

Please sign in to comment.