Session Management

Understanding JWT tokens, cookies, and session lifecycle

Session Management

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.

Session Architecture

The session system consists of three main components:

  • JWT Tokens - Self-contained session data signed with your secret
  • HTTP-Only Cookies - Secure storage mechanism for tokens
  • Session Validation - Server-side verification of token authenticity

JWT Tokens

Token Structure

JWT tokens have three parts separated by dots:

header.payload.signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjp7ImlkIjoiMSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSJ9LCJpYXQiOjE3MDk1ODk2MDAsImV4cCI6MTcxMjE4MTYwMH0.
signature-hash

Token Payload

The 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"
}

Token Creation

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);
}

Token Verification

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 });
}

Cookie Configuration

Cookie Attributes

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
}

Cookie Creation

Creating a Session Cookie

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"

Cookie Clearing

Clearing a Session Cookie

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"

Session Lifecycle

1. Authentication (Login)

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 dashboard

2. Session Validation (Every Request)

Every 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 });

3. Token Refresh (Optional)

Tokens can be refreshed before expiration:

Token Refresh Example

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);
  }
}

4. Sign Out (Logout)

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 login

Session Callbacks

Customize session behavior with callbacks:

User Callback

Resolve or create user after authentication:

User Callback

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,
    };
  },
}

JWT Callback

Add custom claims to the JWT token:

JWT Callback

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;
  },
}

Session Callback

Shape the session object returned to clients:

Session Callback

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;
  },
}

Session Storage

Stateless (Default)

By default, sessions are stateless (JWT-only):

  • Pros: Fast, scalable, no database required
  • Cons: Cannot revoke tokens before expiration
// No adapter - sessions are stateless JWT tokens
const config = {
  secret: process.env.AUTH_SECRET!,
  provider: google({ /* ... */ }),
  // No adapter specified
};

Stateful (With Adapter)

Use a database adapter for stateful sessions:

  • Pros: Can revoke sessions, track active sessions
  • Cons: Requires database, slight performance overhead
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
};

Security Best Practices

1. Strong Secret

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-here

Keep Secret Secure

The AUTH_SECRET is used to sign JWT tokens. If compromised, attackers can forge sessions. Never commit to version control or expose in client code.

2. HTTPS Only

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 attacks

3. HttpOnly Cookies

Prevent 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 cookies

4. SameSite Protection

Prevent 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 requests

5. Short Expiration

Use 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"

Session Types

The SDK supports different session types:

Standard Sessions

{
  type: "oauth",
  user: { id, email, name, picture },
  expires: "2024-12-31T23:59:59.000Z"
}

MCP Agent Sessions

{
  type: "mcp-agent",
  user: { id, email },
  scopes: ["debug", "read"],
  agentId: "claude-dev",
  expires: "2024-03-15T12:30:00.000Z" // Short-lived
}

Troubleshooting

Session Not Persisting

Check cookie configuration:

// Ensure cookies are enabled in browser
// Check cookie domain matches your app
// Verify HTTPS in production (required for secure cookies)

Session Expired

Handle expired sessions gracefully:

Handle Expired Sessions

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();

Token Too Large

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 needed

Cannot Read Cookie

Remember: 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!

Advanced Topics

Custom Token Expiration

Custom Expiration Times

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;
  },
}

Session Revocation

Revoke sessions immediately (requires adapter):

Revoke User Sessions

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 request

Multi-Device Sessions

Track sessions across multiple devices:

List Active Sessions

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>
);

Next Steps