JWT lifecycle, customization, and security best practices
@warpy-auth-sdk/core uses JSON Web Tokens (JWT) for session management. This guide covers token lifecycle, customization, security, and advanced patterns.
The SDK supports two types of tokens:
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;
}interface MCPAgentTokenPayload {
userId: string;
agentId: string;
scopes: string[];
type: 'mcp-agent';
iat: number;
exp: number;
}Tokens are created during authentication flows:
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 });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)
}Tokens are verified on every protected request:
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 });
}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 daysTokens can be revoked explicitly:
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 });
}Use the jwt callback to add custom 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' }
);Decode tokens to access custom 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,
});
}MCP agent tokens are created via the agent_login tool:
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...Use verifyAgentToken for Bearer token authentication:
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,
});
}Revoke agent tokens immediately with the revoke_token tool:
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 failGenerate 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-charactersAUTH_SECRETAlways 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
}Use shorter expiration times for admin or sensitive operations:
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;
},
}Implement token refresh for long-lived sessions:
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 });
}Debug tokens during development:
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);
}Decode and verify tokens using the jwt.io debugger:
AUTH_SECRET in the "Verify Signature" sectionNow that you understand token management, explore: