Token Management

JWT lifecycle, customization, and security best practices

Token Management

@warpy-auth-sdk/core uses JSON Web Tokens (JWT) for session management. This guide covers token lifecycle, customization, security, and advanced patterns.

Token Types

The SDK supports two types of tokens:

  • Standard Tokens - User sessions created via OAuth or email authentication (7-day expiration)
  • MCP Agent Tokens - Short-lived tokens for AI agent delegation (15-minute expiration)

Standard Token Structure

interface StandardTokenPayload {
  userId: string;
  email: string;
  name?: string;
  type: 'standard';
  iat: number;  // Issued at (Unix timestamp)
  exp: number;  // Expiration (Unix timestamp)
  // Custom claims added via jwt callback
  [key: string]: any;
}

MCP Agent Token Structure

interface MCPAgentTokenPayload {
  userId: string;
  agentId: string;
  scopes: string[];
  type: 'mcp-agent';
  iat: number;
  exp: number;
}

Token Lifecycle

1. Token Creation

Tokens are created during authentication flows:

OAuth Token Creation

Standard tokens created after OAuth callback

// Internal flow (automatic)
// 1. User completes OAuth
// 2. callbacks.user resolves user
// 3. callbacks.jwt adds custom claims
const jwtPayload = {
  userId: user.id,
  email: user.email,
  name: user.name,
  type: 'standard',
};

// 4. SDK signs JWT with secret
const token = signJWT(jwtPayload, config.secret);

// 5. Token stored in HttpOnly cookie
const cookie = createSessionCookie({ token, expires });

2. Token Storage

Tokens are stored in secure, HttpOnly cookies:

// Cookie configuration (automatic)
{
  httpOnly: true,      // Not accessible via JavaScript
  secure: true,        // HTTPS only (production)
  sameSite: 'lax',     // CSRF protection
  path: '/',           // Available site-wide
  maxAge: 604800,      // 7 days (standard tokens)
}

HttpOnly Cookies

Tokens are stored in HttpOnly cookies by default, making them inaccessible to client-side JavaScript. This protects against XSS attacks.

3. Token Verification

Tokens are verified on every protected request:

Automatic Token Verification

SDK verifies tokens in getSession()

import { getSession } from '@warpy-auth-sdk/core';

export async function GET(request: Request) {
  // SDK automatically:
  // 1. Extracts token from cookie header
  // 2. Verifies JWT signature with secret
  // 3. Checks expiration
  // 4. Returns session or null
  const session = await getSession(request, process.env.AUTH_SECRET!);

  if (!session) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  return Response.json({ user: session.user });
}

4. Token Expiration

Tokens have configurable expiration times:

// Standard tokens: 7 days
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);

// MCP agent tokens: 15 minutes (configurable)
const mcpExpires = new Date(Date.now() + 15 * 60 * 1000);

// Custom expiration via expiresIn parameter
const customToken = signJWT(payload, secret, '1h'); // 1 hour
const customToken2 = signJWT(payload, secret, '30d'); // 30 days

5. Token Revocation

Tokens can be revoked explicitly:

Token Revocation

Revoke tokens immediately on sign out

import { signOut, clearSessionCookie } from '@warpy-auth-sdk/core';

export async function POST(request: Request) {
  // Revoke token (deletes from adapter if present)
  await signOut(request, {
    secret: process.env.AUTH_SECRET!,
    provider: google({ /* ... */ }),
    adapter: PrismaAdapter(prisma), // Optional
  });

  // Clear cookie
  const headers = new Headers();
  headers.append('Set-Cookie', clearSessionCookie());

  return Response.json({ success: true }, { headers });
}

JWT Customization

Adding Custom Claims

Use the jwt callback to add custom claims:

Custom JWT Claims

Embed roles, permissions, and metadata

import { authMiddleware } from '@warpy-auth-sdk/core/next';

const handler = authMiddleware(
  {
    secret: process.env.AUTH_SECRET!,
    provider: google({ /* ... */ }),
    callbacks: {
      async jwt(token) {
        // Fetch user data
        const user = await prisma.user.findUnique({
          where: { id: token.userId },
          include: {
            roles: true,
            organization: true,
            subscription: true,
          },
        });

        // Add custom claims
        return {
          ...token,
          // Role-based access control
          roles: user?.roles.map(r => r.name) ?? [],
          isAdmin: user?.roles.some(r => r.name === 'admin') ?? false,

          // Organization context
          organizationId: user?.organization?.id,
          organizationSlug: user?.organization?.slug,

          // Subscription info
          subscriptionTier: user?.subscription?.tier,
          subscriptionStatus: user?.subscription?.status,

          // Metadata
          createdAt: user?.createdAt.toISOString(),
          lastLogin: new Date().toISOString(),
        };
      },
    },
  },
  { basePath: '/api/auth' }
);

Accessing JWT Claims

Decode tokens to access custom claims:

Decoding JWT Claims

Access custom claims in your application

import jwt from 'jsonwebtoken';
import { getSession } from '@warpy-auth-sdk/core';

export async function GET(request: Request) {
  const session = await getSession(request, process.env.AUTH_SECRET!);

  if (!session?.token) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Decode token to access custom claims
  const decoded = jwt.decode(session.token) as any;

  // Access custom claims
  const isAdmin = decoded.isAdmin;
  const organizationId = decoded.organizationId;
  const subscriptionTier = decoded.subscriptionTier;

  // Authorization logic
  if (!isAdmin) {
    return Response.json({ error: 'Forbidden' }, { status: 403 });
  }

  return Response.json({
    user: session.user,
    roles: decoded.roles,
    organization: organizationId,
  });
}

Token Size Limits

JWT tokens are stored in cookies, which have a 4KB limit in most browsers. Keep custom claims minimal. For large datasets, store IDs in the token and fetch data server-side.

MCP Agent Tokens

Creating Agent Tokens

MCP agent tokens are created via the agent_login tool:

Creating Agent Tokens

Short-lived tokens for AI agent delegation

import { createMCPTools } from '@warpy-auth-sdk/core';

const mcpTools = createMCPTools({
  secret: process.env.AUTH_SECRET!,
});

// AI agent calls agent_login tool
const result = await mcpTools.agent_login.execute({
  userId: 'user-123',
  scopes: ['debug', 'read'],
  agentId: 'dev-agent',
  expiresIn: '15m', // 15 minutes (default)
});

// Returns short-lived JWT
console.log(result.token); // eyJhbGci...

Verifying Agent Tokens

Use verifyAgentToken for Bearer token authentication:

Verifying Agent Tokens

Validate and check scopes for agent requests

import { verifyAgentToken } from '@warpy-auth-sdk/core';

export async function GET(request: Request) {
  // Verify token from Authorization header
  const session = await verifyAgentToken(
    request,
    process.env.AUTH_SECRET!
  );

  if (!session) {
    return Response.json({ error: 'Invalid token' }, { status: 401 });
  }

  // Check token type
  if (session.type !== 'mcp-agent') {
    return Response.json({ error: 'Not an agent token' }, { status: 403 });
  }

  // Check scopes
  if (!session.scopes?.includes('debug')) {
    return Response.json({ error: 'Missing debug scope' }, { status: 403 });
  }

  // Agent is authorized
  return Response.json({
    agentId: session.agentId,
    scopes: session.scopes,
  });
}

Agent Token Revocation

Revoke agent tokens immediately with the revoke_token tool:

Revoking Agent Tokens

Immediately invalidate agent tokens

import { createMCPTools } from '@warpy-auth-sdk/core';

const mcpTools = createMCPTools({
  secret: process.env.AUTH_SECRET!,
});

// Revoke token
await mcpTools.revoke_token.execute({
  token: 'eyJhbGci...',
});

// Token is now invalid
// Subsequent verifyAgentToken() calls will fail

Token Security Best Practices

1. Secure Secret Management

Strong Secret Generation

Generate cryptographically secure secrets

# Generate a strong secret (Node.js)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

# Or use openssl
openssl rand -hex 32

# Store in environment variables
# .env.local
AUTH_SECRET=your-generated-secret-here-minimum-32-characters

Secret Security

  • Use at least 32 characters for AUTH_SECRET
  • Never commit secrets to version control
  • Use different secrets for development and production
  • Rotate secrets periodically

2. HttpOnly Cookies

Always use HttpOnly cookies for token storage (default in SDK):

// Automatic in SDK - tokens are HttpOnly by default
{
  httpOnly: true,  // ✅ Not accessible via document.cookie
  secure: true,    // ✅ HTTPS only in production
  sameSite: 'lax', // ✅ CSRF protection
}

3. Short Expiration for Sensitive Operations

Use shorter expiration times for admin or sensitive operations:

Short-Lived Admin Tokens

Re-authenticate for sensitive operations

callbacks: {
  async jwt(token) {
    const user = await prisma.user.findUnique({
      where: { id: token.userId },
      include: { roles: true },
    });

    const isAdmin = user?.roles.some(r => r.name === 'admin');

    if (isAdmin) {
      // Admin tokens expire after 1 hour instead of 7 days
      // Implement via custom expiration logic
      return {
        ...token,
        isAdmin: true,
        adminExpiresAt: Date.now() + 60 * 60 * 1000, // 1 hour
      };
    }

    return token;
  },

  session(session) {
    const decoded = jwt.decode(session.token!) as any;

    // Check admin token expiration
    if (decoded.isAdmin && decoded.adminExpiresAt < Date.now()) {
      throw new Error('Admin session expired. Please re-authenticate.');
    }

    return session;
  },
}

4. Token Refresh

Implement token refresh for long-lived sessions:

Token Refresh Pattern

Refresh tokens before expiration

// Client-side token refresh
useEffect(() => {
  const refreshInterval = setInterval(async () => {
    // Refresh session before expiration
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      credentials: 'include', // Include cookies
    });

    if (!response.ok) {
      // Redirect to login if refresh fails
      window.location.href = '/login';
    }
  }, 6 * 24 * 60 * 60 * 1000); // Refresh every 6 days (before 7-day expiration)

  return () => clearInterval(refreshInterval);
}, []);

// Server-side refresh endpoint
// app/api/auth/refresh/route.ts
import { getSession, createSessionCookie } from '@warpy-auth-sdk/core';

export async function POST(request: Request) {
  const session = await getSession(request, process.env.AUTH_SECRET!);

  if (!session) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Create new token with extended expiration
  const newSession = {
    ...session,
    expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
  };

  const headers = new Headers();
  headers.append('Set-Cookie', createSessionCookie(newSession));

  return Response.json({ success: true }, { headers });
}

Debugging Tokens

Inspecting Token Contents

Debug tokens during development:

Token Debugging

Inspect token claims and expiration

import jwt from 'jsonwebtoken';

// Decode without verification (for debugging only)
const decoded = jwt.decode(token);
console.log('Token payload:', decoded);

// Verify and decode
try {
  const verified = jwt.verify(token, process.env.AUTH_SECRET!) as any;
  console.log('Verified payload:', verified);
  console.log('Issued at:', new Date(verified.iat * 1000));
  console.log('Expires at:', new Date(verified.exp * 1000));
  console.log('Time until expiration:', verified.exp * 1000 - Date.now());
} catch (error) {
  console.error('Token verification failed:', error);
}

Using jwt.io

Decode and verify tokens using the jwt.io debugger:

  1. Copy your JWT token from cookies or logs
  2. Paste into jwt.io debugger
  3. Paste your AUTH_SECRET in the "Verify Signature" section
  4. Inspect decoded payload and verify signature

Security Warning

Never paste production secrets or tokens into public tools. Use jwt.io only for development/debugging.

Next Steps

Now that you understand token management, explore:

Token Management | @warpy-auth-sdk/core