Understanding JWT tokens, cookies, and session lifecycle
The SDK uses JWT (JSON Web Tokens) and HTTP-only cookies for secure, stateless session management. This approach provides the best balance of security, performance, and scalability.
The session system consists of three main components:
JWT tokens have three parts separated by dots:
header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjp7ImlkIjoiMSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSJ9LCJpYXQiOjE3MDk1ODk2MDAsImV4cCI6MTcxMjE4MTYwMH0.
signature-hashThe payload contains session data:
{
// User information
"user": {
"id": "user-123",
"email": "user@example.com",
"name": "John Doe",
"picture": "https://..."
},
// Standard JWT claims
"iat": 1709589600, // Issued at (timestamp)
"exp": 1712181600, // Expiration (timestamp)
// Custom claims (from jwt callback)
"role": "admin",
"subscriptionTier": "pro"
}Tokens are created automatically during authentication:
import { authenticate, createSessionCookie } from "@warpy-auth-sdk/core";
const result = await authenticate(config, request);
if (result.session) {
// Token is signed with your AUTH_SECRET
const cookie = createSessionCookie(result.session, config.secret);
// Set the cookie in the response
response.headers.set("Set-Cookie", cookie);
}Tokens are verified on every request:
import { getSession } from "@warpy-auth-sdk/core";
// Automatically verifies signature and expiration
const session = await getSession(request, secret);
if (session) {
// Token is valid and not expired
console.log(session.user.email);
} else {
// Token is invalid, expired, or missing
return Response.json({ error: "Unauthorized" }, { status: 401 });
}The SDK uses secure cookie defaults:
{
name: "auth-session", // Cookie name
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only (production)
sameSite: "lax", // CSRF protection
maxAge: 60 * 60 * 24 * 30, // 30 days in seconds
path: "/" // Available on all routes
}Manual cookie creation
import { createSessionCookie } from "@warpy-auth-sdk/core";
const session = {
user: {
id: "user-123",
email: "user@example.com",
name: "John Doe",
},
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
};
const cookie = createSessionCookie(session, process.env.AUTH_SECRET!);
// Cookie string:
// "auth-session=<jwt-token>; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000"Sign out by clearing the cookie
import { clearSessionCookie } from "@warpy-auth-sdk/core";
// Returns an expired cookie to clear the session
const cookie = clearSessionCookie();
// Cookie string:
// "auth-session=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0"User authenticates and receives a session token:
// 1. User clicks "Sign in with Google"
// 2. OAuth flow completes
// 3. authenticate() is called
const result = await authenticate(config, request);
// 4. Session is created
const session = result.session;
// 5. JWT token is signed
// 6. Cookie is set in response
const cookie = createSessionCookie(session, secret);
response.headers.set("Set-Cookie", cookie);
// 7. User is redirected to dashboardEvery request validates the session token:
// 1. Request includes session cookie
// 2. getSession() extracts and verifies JWT
const session = await getSession(request, secret);
// 3. If valid, session data is available
if (session) {
// User is authenticated
return Response.json({ user: session.user });
}
// 4. If invalid/expired, session is null
return Response.json({ error: "Unauthorized" }, { status: 401 });Tokens can be refreshed before expiration:
Extend session expiration
import { getSession, createSessionCookie } from "@warpy-auth-sdk/core";
const session = await getSession(request, secret);
if (session) {
// Check if session is close to expiring (e.g., < 7 days)
const expiresAt = new Date(session.expires);
const sevenDaysFromNow = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
if (expiresAt < sevenDaysFromNow) {
// Create new session with extended expiration
const newSession = {
...session,
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
};
// Set new cookie
const cookie = createSessionCookie(newSession, secret);
response.headers.set("Set-Cookie", cookie);
}
}User signs out and session is cleared:
// 1. User clicks "Sign Out"
// 2. signOut() is called
await signOut(request, secret);
// 3. Optional: Revoke token (if using adapter)
// 4. Cookie is cleared
const cookie = clearSessionCookie();
response.headers.set("Set-Cookie", cookie);
// 5. User is redirected to loginCustomize session behavior with callbacks:
Resolve or create user after authentication:
Save user to database
callbacks: {
async user(oauthUser) {
// Called after successful OAuth
// oauthUser: { id, email, name, picture }
// Save to database
const user = await db.user.upsert({
where: { email: oauthUser.email },
create: {
email: oauthUser.email,
name: oauthUser.name,
picture: oauthUser.picture,
},
update: {
name: oauthUser.name,
picture: oauthUser.picture,
},
});
// Return user for session
return {
id: user.id,
email: user.email,
name: user.name,
picture: user.picture,
};
},
}Add custom claims to the JWT token:
Add role and permissions
callbacks: {
jwt(token) {
// Add custom claims to JWT payload
// Example: Add role from database
token.role = "admin";
token.permissions = ["read", "write", "delete"];
// Example: Add subscription tier
token.subscriptionTier = "pro";
return token;
},
}Shape the session object returned to clients:
Customize session object
callbacks: {
session(session) {
// Add custom data to session
// Example: Add preferences
session.preferences = {
theme: "dark",
language: "en",
};
// Example: Add metadata
session.metadata = {
lastLoginAt: new Date().toISOString(),
};
return session;
},
}By default, sessions are stateless (JWT-only):
// No adapter - sessions are stateless JWT tokens
const config = {
secret: process.env.AUTH_SECRET!,
provider: google({ /* ... */ }),
// No adapter specified
};Use a database adapter for stateful sessions:
import { PrismaAdapter } from "@warpy-auth-sdk/core";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const config = {
secret: process.env.AUTH_SECRET!,
provider: google({ /* ... */ }),
adapter: PrismaAdapter(prisma), // Sessions stored in database
};Use a strong, random secret (min 32 characters):
# Generate a secure secret
openssl rand -base64 32
# Store in .env.local
AUTH_SECRET=your-generated-secret-hereAUTH_SECRET is used to sign JWT tokens. If compromised, attackers can forge sessions. Never commit to version control or expose in client code.Always use HTTPS in production:
// Cookies are only sent over HTTPS when secure: true
const cookie = createSessionCookie(session, secret);
// In production, this prevents man-in-the-middle attacksPrevent XSS attacks with httpOnly:
// httpOnly: true prevents JavaScript access
// Protects against XSS (cross-site scripting)
// ✗ This won't work (httpOnly cookie)
document.cookie; // Cannot read auth-session
// ✓ Correct - let the server handle cookiesPrevent CSRF attacks with sameSite:
// sameSite: "lax" prevents CSRF attacks
// Cookie only sent with same-site requests
// ✗ Malicious site cannot trigger authenticated requests
// ✓ Your site can make authenticated requestsUse appropriate token expiration times:
// Standard sessions: 30 days
maxAge: 60 * 60 * 24 * 30
// Sensitive operations: 1 hour
maxAge: 60 * 60
// MCP agent tokens: 15 minutes
expiresIn: "15m"The SDK supports different session types:
{
type: "oauth",
user: { id, email, name, picture },
expires: "2024-12-31T23:59:59.000Z"
}{
type: "mcp-agent",
user: { id, email },
scopes: ["debug", "read"],
agentId: "claude-dev",
expires: "2024-03-15T12:30:00.000Z" // Short-lived
}Check cookie configuration:
// Ensure cookies are enabled in browser
// Check cookie domain matches your app
// Verify HTTPS in production (required for secure cookies)Handle expired sessions gracefully:
Redirect to login on expiration
const session = await getSession(request, secret);
if (!session) {
// Session expired or invalid
return NextResponse.redirect(new URL("/login", request.url));
}
// Session is valid
return NextResponse.next();Keep JWT payload small:
// ✗ Don't store large data in JWT
jwt(token) {
token.allUserPosts = /* hundreds of posts */; // Too large!
return token;
}
// ✓ Store references only
jwt(token) {
token.userId = "user-123"; // Small reference
return token;
}
// Fetch full data from database when neededRemember: httpOnly cookies cannot be read from JavaScript:
// ✗ This won't work
const token = document.cookie; // Cannot access httpOnly cookie
// ✓ Use the SDK's hooks/functions
const { session } = useAuth(); // Works!
const session = await getSession(request, secret); // Works!Different expiration for different user types
callbacks: {
jwt(token) {
// Admin sessions last 7 days
if (token.role === "admin") {
token.exp = Math.floor(Date.now() / 1000) + (7 * 24 * 60 * 60);
}
// Regular users: 30 days
else {
token.exp = Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60);
}
return token;
},
}Revoke sessions immediately (requires adapter):
Sign out user from all devices
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
// Delete all sessions for a user
await prisma.session.deleteMany({
where: { userId: "user-123" },
});
// User will be signed out on next requestTrack sessions across multiple devices:
Show user their active sessions
// Fetch all active sessions for user
const sessions = await prisma.session.findMany({
where: {
userId: session.user.id,
expires: { gt: new Date() },
},
orderBy: { createdAt: "desc" },
});
// Display to user with option to revoke
return (
<div>
<h2>Active Sessions</h2>
{sessions.map((s) => (
<div key={s.id}>
<p>Device: {s.userAgent}</p>
<p>Last Active: {s.updatedAt}</p>
<button onClick={() => revokeSession(s.id)}>
Revoke
</button>
</div>
))}
</div>
);