Session Management
Patterns for managing user sessions with JWT and database storage.
Overview#
Session management enables:
- User state persistence across requests
- Multi-device session tracking
- Session revocation and security
- Activity monitoring
Prerequisites:
- Authentication system (Clerk, NextAuth, or custom)
- Database for session storage (optional)
Implementation#
JWT Session Configuration#
1// auth.config.ts
2import { NextAuthConfig } from 'next-auth'
3
4export const authConfig: NextAuthConfig = {
5 session: {
6 strategy: 'jwt',
7 maxAge: 30 * 24 * 60 * 60, // 30 days
8 updateAge: 24 * 60 * 60 // 24 hours
9 },
10 callbacks: {
11 jwt({ token, user, trigger, session }) {
12 // Initial sign in
13 if (user) {
14 token.id = user.id
15 token.role = user.role
16 token.organizationId = user.organizationId
17 }
18
19 // Update session when trigger is 'update'
20 if (trigger === 'update' && session) {
21 token.name = session.name
22 token.organizationId = session.organizationId
23 }
24
25 return token
26 },
27 session({ session, token }) {
28 session.user.id = token.id as string
29 session.user.role = token.role as string
30 session.user.organizationId = token.organizationId as string
31
32 return session
33 }
34 }
35}Database Sessions#
1// auth.ts
2import NextAuth from 'next-auth'
3import { PrismaAdapter } from '@auth/prisma-adapter'
4import { prisma } from '@/lib/db'
5
6export const { handlers, auth, signIn, signOut } = NextAuth({
7 adapter: PrismaAdapter(prisma),
8 session: {
9 strategy: 'database',
10 maxAge: 30 * 24 * 60 * 60, // 30 days
11 updateAge: 24 * 60 * 60 // Update session every 24 hours
12 },
13 callbacks: {
14 session({ session, user }) {
15 session.user.id = user.id
16 session.user.role = user.role
17 return session
18 }
19 },
20 providers: [
21 // ... providers
22 ]
23})Session Refresh#
1// lib/auth/session.ts
2import { auth, signIn } from '@/auth'
3import { redirect } from 'next/navigation'
4
5export async function getSession() {
6 const session = await auth()
7
8 if (!session) {
9 return null
10 }
11
12 // Check if session is about to expire
13 const expiresAt = new Date(session.expires)
14 const now = new Date()
15 const daysUntilExpiry = (expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
16
17 if (daysUntilExpiry < 7) {
18 // Session will expire soon, refresh it
19 // This triggers the updateAge callback
20 }
21
22 return session
23}
24
25export async function requireAuth() {
26 const session = await getSession()
27
28 if (!session) {
29 redirect('/login')
30 }
31
32 return session
33}
34
35export async function requireRole(requiredRoles: string[]) {
36 const session = await requireAuth()
37
38 if (!requiredRoles.includes(session.user.role)) {
39 redirect('/unauthorized')
40 }
41
42 return session
43}Session Update#
1// components/UpdateSessionForm.tsx
2'use client'
3
4import { useSession } from 'next-auth/react'
5import { useState } from 'react'
6
7export function UpdateSessionForm() {
8 const { data: session, update } = useSession()
9 const [name, setName] = useState(session?.user?.name ?? '')
10
11 const handleSubmit = async (e: React.FormEvent) => {
12 e.preventDefault()
13
14 // Update user in database
15 await fetch('/api/user/profile', {
16 method: 'PATCH',
17 body: JSON.stringify({ name })
18 })
19
20 // Update session
21 await update({ name })
22 }
23
24 return (
25 <form onSubmit={handleSubmit}>
26 <input
27 type="text"
28 value={name}
29 onChange={e => setName(e.target.value)}
30 />
31 <button type="submit">Update</button>
32 </form>
33 )
34}Active Sessions Management#
1// prisma/schema.prisma
2model Session {
3 id String @id @default(cuid())
4 sessionToken String @unique
5 userId String
6 expires DateTime
7 user User @relation(fields: [userId], references: [id], onDelete: Cascade)
8
9 // Additional fields for session management
10 userAgent String?
11 ipAddress String?
12 lastActive DateTime @default(now())
13 createdAt DateTime @default(now())
14
15 @@index([userId])
16}1// lib/auth/sessions.ts
2import { prisma } from '@/lib/db'
3import { headers } from 'next/headers'
4
5export async function getUserSessions(userId: string) {
6 return prisma.session.findMany({
7 where: { userId },
8 orderBy: { lastActive: 'desc' },
9 select: {
10 id: true,
11 userAgent: true,
12 ipAddress: true,
13 lastActive: true,
14 createdAt: true,
15 expires: true
16 }
17 })
18}
19
20export async function revokeSession(sessionId: string, userId: string) {
21 return prisma.session.deleteMany({
22 where: {
23 id: sessionId,
24 userId // Ensure user owns the session
25 }
26 })
27}
28
29export async function revokeAllOtherSessions(
30 currentSessionToken: string,
31 userId: string
32) {
33 return prisma.session.deleteMany({
34 where: {
35 userId,
36 NOT: { sessionToken: currentSessionToken }
37 }
38 })
39}
40
41export async function updateSessionActivity(sessionToken: string) {
42 const headersList = headers()
43
44 await prisma.session.update({
45 where: { sessionToken },
46 data: {
47 lastActive: new Date(),
48 userAgent: headersList.get('user-agent'),
49 ipAddress:
50 headersList.get('x-forwarded-for') ??
51 headersList.get('x-real-ip')
52 }
53 })
54}Session List UI#
1// app/settings/sessions/page.tsx
2import { auth } from '@/auth'
3import { getUserSessions, revokeSession } from '@/lib/auth/sessions'
4import { formatDistanceToNow } from 'date-fns'
5import { UAParser } from 'ua-parser-js'
6
7export default async function SessionsPage() {
8 const session = await auth()
9 if (!session) return null
10
11 const sessions = await getUserSessions(session.user.id)
12
13 return (
14 <div className="space-y-6">
15 <h2 className="text-xl font-semibold">Active Sessions</h2>
16
17 <div className="space-y-4">
18 {sessions.map(s => {
19 const ua = new UAParser(s.userAgent ?? '').getResult()
20 const isCurrent = s.id === session.sessionId
21
22 return (
23 <div
24 key={s.id}
25 className="flex items-center justify-between rounded-lg border p-4"
26 >
27 <div>
28 <div className="flex items-center gap-2">
29 <span className="font-medium">
30 {ua.browser.name} on {ua.os.name}
31 </span>
32 {isCurrent && (
33 <span className="rounded bg-green-100 px-2 py-0.5 text-xs text-green-800">
34 Current
35 </span>
36 )}
37 </div>
38 <div className="text-sm text-gray-500">
39 <span>{s.ipAddress}</span>
40 <span className="mx-2">-</span>
41 <span>
42 Last active {formatDistanceToNow(s.lastActive, { addSuffix: true })}
43 </span>
44 </div>
45 </div>
46
47 {!isCurrent && (
48 <form action={async () => {
49 'use server'
50 await revokeSession(s.id, session.user.id)
51 }}>
52 <button
53 type="submit"
54 className="text-sm text-red-600 hover:underline"
55 >
56 Revoke
57 </button>
58 </form>
59 )}
60 </div>
61 )
62 })}
63 </div>
64 </div>
65 )
66}Session Middleware#
1// middleware.ts
2import { NextRequest, NextResponse } from 'next/server'
3import { auth } from '@/auth'
4
5export async function middleware(request: NextRequest) {
6 const session = await auth()
7
8 // Check if session exists for protected routes
9 if (request.nextUrl.pathname.startsWith('/dashboard')) {
10 if (!session) {
11 return NextResponse.redirect(new URL('/login', request.url))
12 }
13
14 // Check session expiry
15 if (new Date(session.expires) < new Date()) {
16 const response = NextResponse.redirect(new URL('/login', request.url))
17 // Clear invalid session cookie
18 response.cookies.delete('next-auth.session-token')
19 return response
20 }
21 }
22
23 // Add session info to headers for logging
24 const requestHeaders = new Headers(request.headers)
25 if (session?.user) {
26 requestHeaders.set('x-user-id', session.user.id)
27 }
28
29 return NextResponse.next({
30 request: { headers: requestHeaders }
31 })
32}
33
34export const config = {
35 matcher: ['/dashboard/:path*', '/api/:path*']
36}Best Practices#
- Track session metadata - Store user agent, IP, and last active time
- Enable session revocation - Allow users to sign out of other devices
- Set reasonable expiry - Balance security with user convenience
- Update activity timestamps - Track last active time for idle detection
- Use secure cookies - httpOnly, secure, sameSite flags