Session Management
intermediateSecure session handling with database storage and automatic cleanup.
authsessionsecurity
Tested on⬢20▲16⚛19TS5.9
$ bunx sinew add auth/sessions1The Problem
Session management is easy to get wrong:
- JWTs can't be invalidated without additional infrastructure
- Session fixation attacks
- No visibility into active sessions
- Expired sessions accumulate in the database
2The Solution
Use database-backed sessions with Auth.js for full control over session lifecycle, with automatic cleanup of expired sessions.
3Files
lib/auth.ts
lib/auth.tsTypeScript
import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "./db";
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
session: {
strategy: "database",
maxAge: 30 * 24 * 60 * 60, // 30 days
updateAge: 24 * 60 * 60, // Update session every 24 hours
},
callbacks: {
session({ session, user }) {
session.user.id = user.id;
return session;
},
},
});lib/session.ts
lib/session.tsTypeScript
import { prisma } from "./db";
import { auth } from "./auth";
export async function getCurrentSession() {
const session = await auth();
if (!session?.user?.id) return null;
return prisma.session.findFirst({
where: {
userId: session.user.id,
expires: { gt: new Date() },
},
orderBy: { expires: "desc" },
});
}
export async function getUserSessions(userId: string) {
return prisma.session.findMany({
where: {
userId,
expires: { gt: new Date() },
},
orderBy: { expires: "desc" },
});
}
export async function revokeSession(sessionToken: string, userId: string) {
// Only allow revoking own sessions
return prisma.session.deleteMany({
where: {
sessionToken,
userId,
},
});
}
export async function revokeAllSessions(userId: string, exceptCurrent?: string) {
return prisma.session.deleteMany({
where: {
userId,
...(exceptCurrent && {
NOT: { sessionToken: exceptCurrent },
}),
},
});
}
export async function cleanupExpiredSessions() {
const result = await prisma.session.deleteMany({
where: {
expires: { lt: new Date() },
},
});
console.info(`Cleaned up ${result.count} expired sessions`);
return result.count;
}app/api/sessions/route.ts
app/api/sessions/route.tsTypeScript
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { getUserSessions, revokeSession, revokeAllSessions } from "@/lib/session";
export async function GET() {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const sessions = await getUserSessions(session.user.id);
return NextResponse.json(
sessions.map((s) => ({
id: s.sessionToken.slice(0, 8), // Partial token for identification
expires: s.expires,
isCurrent: false, // Can be determined by comparing with current session
}))
);
}
export async function DELETE(req: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { sessionToken, all } = await req.json();
if (all) {
// Revoke all sessions except current
await revokeAllSessions(session.user.id, sessionToken);
} else if (sessionToken) {
await revokeSession(sessionToken, session.user.id);
}
return NextResponse.json({ success: true });
}app/api/cron/cleanup-sessions/route.ts
app/api/cron/cleanup-sessions/route.tsTypeScript
import { NextRequest, NextResponse } from "next/server";
import { cleanupExpiredSessions } from "@/lib/session";
export async function GET(req: NextRequest) {
// Verify cron secret
const authHeader = req.headers.get("authorization");
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const count = await cleanupExpiredSessions();
return NextResponse.json({ cleaned: count });
}components/session-manager.tsx
components/session-manager.tsxTypeScript
"use client";
import { useEffect, useState } from "react";
interface Session {
id: string;
expires: string;
isCurrent: boolean;
}
export function SessionManager() {
const [sessions, setSessions] = useState<Session[]>([]);
useEffect(() => {
fetch("/api/sessions")
.then((res) => res.json())
.then(setSessions);
}, []);
async function revokeSession(sessionId: string) {
await fetch("/api/sessions", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionToken: sessionId }),
});
setSessions((prev) => prev.filter((s) => s.id !== sessionId));
}
async function revokeAllOther() {
await fetch("/api/sessions", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ all: true }),
});
setSessions((prev) => prev.filter((s) => s.isCurrent));
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Active Sessions</h2>
<button
onClick={revokeAllOther}
className="text-sm text-red-500 hover:text-red-600"
>
Sign out all other devices
</button>
</div>
<div className="space-y-2">
{sessions.map((session) => (
<div
key={session.id}
className="flex justify-between items-center p-3 rounded-lg border"
>
<div>
<p className="font-mono text-sm">{session.id}...</p>
<p className="text-sm text-muted">
Expires: {new Date(session.expires).toLocaleDateString()}
</p>
</div>
{!session.isCurrent && (
<button
onClick={() => revokeSession(session.id)}
className="text-sm text-red-500"
>
Revoke
</button>
)}
</div>
))}
</div>
</div>
);
}prisma/schema.prisma
prisma/schema.prismaprisma
model Session {
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([expires])
}4Configuration
Session Options
session: {
strategy: "database", // Use database sessions
maxAge: 30 * 24 * 60 * 60, // 30 days
updateAge: 24 * 60 * 60, // Extend session every 24 hours on activity
}TypeScript
Cron Job for Cleanup
Add to vercel.json:
{
"crons": [
{
"path": "/api/cron/cleanup-sessions",
"schedule": "0 0 * * *"
}
]
}JSON
5Usage
Get Current Session
import { auth } from "@/lib/auth";
const session = await auth();
if (session?.user) {
// User is authenticated
}TypeScript
Force Re-authentication
// Revoke all sessions to force re-login on all devices
await revokeAllSessions(userId);TypeScript
6Troubleshooting
Session not persisting
- Check that database adapter is configured correctly
- Verify session strategy is set to "database"
- Check for cookie configuration issues
Sessions not expiring
- Ensure cron job is running
- Check
maxAgeconfiguration - Verify database indexes on
expirescolumn