Session token rotation & Verification pattern in Next.js 15 (Database-backed sessions) #78001
Replies: 2 comments
-
UpdateAfter experimenting and thinking the problem in detail, I realized that indeed middleware is the right place to handle session rotation. The key insight is: Middleware can securely and efficiently rotate sessions because:
My new setup looks like this: Middleware (middleware.js): export default async function middleware(request) {
const encryptedSession = request.cookies.get(SESSION_COOKIE)?.value
if (!encryptedSession) return NextResponse.next()
const res = await fetch(`${process.env.API_URL}/session/${encryptedSession}`, {
method: "POST",
headers: { "x-edge-auth": process.env.EDGE_API_SECRET },
})
if (res.ok) {
const data = await res.json()
if (data.newSession) {
const response = NextResponse.next()
response.cookies.set(SESSION_COOKIE, data.newSession, {
expires: new Date(data.expiresAt),
httpOnly: true, secure: true, sameSite: "lax"
})
return response
}
}
return NextResponse.next()
} Backend API (app/api/session/[encryptedSession]/route.js): export async function POST(request, { params }) {
const authHeader = request.headers.get("x-edge-auth")
if (authHeader !== process.env.EDGE_API_SECRET) {
return new Response("Unauthorized", { status: 401 })
}
const { encryptedSession } = params
const token = await decrypt(encryptedSession)
const session = await getSessionByToken(token)
if (!session || session.expiresAt < new Date()) {
return Response.json({ error: "Session expired" }, { status: 401 })
}
const shouldRotate = (new Date(session.expiresAt) - Date.now()) < ONE_DAY_MS
if (!shouldRotate) return Response.json({})
// Rotate token
const newToken = generateToken(64)
const newExpiry = new Date(Date.now() + ONE_DAY_MS)
await createSession(session.userId, newExpiry, newToken)
await deleteSession(token)
const newEncryptedSession = await encrypt(newToken)
return Response.json({ newSession: newEncryptedSession, expiresAt: newExpiry })
} Now, my Server Component logic (verifySession()) became simpler, since it doesn't have to rotate cookies directly, it just validates the session from the DB: export const verifySession = cache(async () => {
const sessionCookie = cookies().get(SESSION_COOKIE)
if (!sessionCookie) throw new Error("No session cookie")
const token = await decrypt(sessionCookie.value)
const dbSession = await getSessionByToken(token)
if (!dbSession || dbSession.expiresAt < new Date()) {
throw new Error("Invalid session")
}
return { token, user: dbSession.user }
}) This separation feels clean:
Hope this helps someone else running into the same architectural friction! 😊 Appreciate any advice 😇 |
Beta Was this translation helpful? Give feedback.
-
Hi. Thank you for the uploaded code. Since middleware runs once for each request that the browser makes to the server, that will lead to multiple simultaneous requests to api route on each middleware run. In your case it works fine, since you implemented additional check if the route should rotate the token in a route itself, but request is anyway being sent multiple times, which is not ideal. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Hi 😁
I'm building an authentication system in Next.js 15 using encrypted tokens stored in HTTP-only cookies and session data saved in a database. Everything is working well, but I’m trying to implement token rotation (i.e. rotating the session token when it’s close to expiring) — and I’m hitting some architectural friction.
The key question:
Here’s what I’m doing now:
Here’s a simplified version of the function:
This works great except that cookies().set() can’t run in a Server Component. So I can’t safely update the cookie when rotating the token — and that breaks the flow.
What if middleware is the right place to handle token rotation?
On the surface it seems like a great fit. But my token rotation logic depends on the expiresAt field from the database..
Would love to hear how you all are approaching this. Appreciate any advice or examples 😇
Beta Was this translation helpful? Give feedback.
All reactions