Authentication
Capstan's auth package (@zauso-ai/capstan-auth) provides dual authentication: JWT sessions for human users and API key authentication for AI agents.
Overview
The auth middleware resolves credentials from incoming requests in this order:
- Session cookie (
capstan_session) -- verifies JWT, returns a human auth context - Authorization header (
Bearer <token>) -- if the token matches the API key prefix, looks up the agent credential and verifies the key hash - Anonymous fallback -- returns
{ type: "anonymous", isAuthenticated: false }
JWT Sessions
Signing a Session
Create a signed JWT for a human user after login:
import { signSession } from "@zauso-ai/capstan-auth";
const token = signSession(
{
userId: "user_123",
email: "alice@example.com",
role: "admin",
},
process.env.SESSION_SECRET!,
"7d", // max age (optional, defaults to "7d")
);
// Set as cookie in response
response.headers.set(
"Set-Cookie",
`capstan_session=${token}; Path=/; HttpOnly; SameSite=Lax`,
);Session Payload
interface SessionPayload {
userId: string;
email?: string;
role?: string;
iat: number; // Issued at (Unix timestamp, set automatically)
exp: number; // Expires at (Unix timestamp, set automatically)
}Verifying a Session
import { verifySession } from "@zauso-ai/capstan-auth";
const payload = verifySession(token, process.env.SESSION_SECRET!);
// Returns SessionPayload on success, null on failure
// Checks: HMAC-SHA256 signature, expirationVerification is timing-safe to prevent timing side-channel attacks.
Duration Format
| Suffix | Unit | Example | Seconds |
|---|---|---|---|
s | seconds | "30s" | 30 |
m | minutes | "30m" | 1,800 |
h | hours | "1h" | 3,600 |
d | days | "7d" | 604,800 |
w | weeks | "2w" | 1,209,600 |
API Key Authentication
API keys are designed for AI agent authentication. They use a prefix-based lookup scheme for efficient database queries and SHA-256 hashing for secure storage.
Generating an API Key
import { generateApiKey } from "@zauso-ai/capstan-auth";
const { key, hash, prefix } = generateApiKey();
// key: "cap_ak_a1b2c3d4e5f6..." (show to user once, never store)
// hash: "sha256hexdigest..." (store in database)
// prefix: "cap_ak_a1b2c3d4" (store for fast DB lookup)Key structure: 128 bits of entropy (32 hex characters), 8-character lookup prefix for indexed database queries. The default prefix is cap_ak_ and can be customized.
Verifying an API Key
import { verifyApiKey } from "@zauso-ai/capstan-auth";
const isValid = await verifyApiKey(plaintextKey, storedHash);
// Returns boolean, uses timing-safe comparisonAgent Credential Storage
interface AgentCredential {
id: string;
name: string; // Human-readable agent name
apiKeyHash: string; // SHA-256 hex digest
apiKeyPrefix: string; // For indexed DB lookup
permissions: string[]; // e.g. ["ticket:read", "ticket:write"]
revokedAt?: string; // ISO timestamp if revoked
}Auth Context
createAuthMiddleware() returns a function that resolves auth context from a request:
import { createAuthMiddleware } from "@zauso-ai/capstan-auth";
const resolveAuth = createAuthMiddleware(
{
session: {
secret: process.env.SESSION_SECRET!,
maxAge: "7d",
},
apiKeys: {
prefix: "cap_ak_",
headerName: "Authorization",
},
},
{
findAgentByKeyPrefix: async (prefix) => {
return db.query.agentCredentials.findFirst({
where: eq(agentCredentials.apiKeyPrefix, prefix),
});
},
},
);
const authContext = await resolveAuth(request);
// authContext.isAuthenticated: boolean
// authContext.type: "human" | "agent" | "anonymous"OAuth Providers
Capstan ships built-in OAuth provider helpers for social login. Use googleProvider() or githubProvider() to configure a provider, then createOAuthHandlers() to get route handlers that manage the full authorization code flow.
import {
googleProvider,
githubProvider,
createOAuthHandlers,
} from "@zauso-ai/capstan-auth";
const oauthHandlers = createOAuthHandlers({
providers: [
googleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
githubProvider({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
],
callbackPath: "/auth/callback",
sessionSecret: process.env.SESSION_SECRET!,
});Login Flow
- User visits
/auth/login/google(or/auth/login/github) - Capstan sets a
capstan_oauth_statecookie and redirects to the provider - After the user authorizes, the provider redirects back to
/auth/callback - Capstan validates the state parameter, exchanges the code for an access token, fetches user info, creates a signed JWT session, and sets the
capstan_sessioncookie - User is redirected to
/as an authenticated human session
DPoP (Sender-Constrained Tokens)
Capstan supports Demonstrating Proof-of-Possession (RFC 9449) to bind access tokens to a specific client key pair. This prevents token replay if a bearer token is intercepted.
auth: {
session: {
strategy: "jwt",
secret: env("SESSION_SECRET"),
dpop: true, // Require DPoP proof on token-protected requests
},
},When dpop: true is set, the auth middleware validates the DPoP header alongside the Authorization header. Requests missing a valid DPoP proof receive a 401 with a DPoP-Nonce header for retry.
SPIFFE / mTLS (Agent Workload Identity)
For service-to-service (agent-to-agent) communication, Capstan supports SPIFFE-based workload identity via mTLS. The X-Client-Cert header (set by a TLS-terminating proxy) is verified against trusted SPIFFE IDs.
auth: {
workloadIdentity: {
trustDomain: "example.org",
trustedDomains: ["example.org", "partner.com"],
certHeader: "X-Client-Cert",
},
},When a request arrives with a valid client certificate, the auth context is populated with type: "agent" and the SPIFFE ID as the agent identifier.
Rate Limiting
defineRateLimit() configures request rate limiting with per-auth-type windows:
import { defineRateLimit } from "@zauso-ai/capstan-core";
export const apiLimits = defineRateLimit({
default: { requests: 100, window: "1m" },
perAuthType: {
anonymous: { requests: 20, window: "1m" },
human: { requests: 200, window: "1m" },
agent: { requests: 1000, window: "1m" },
},
});Apply rate limiting in the config:
export default defineConfig({
agent: {
rateLimit: {
default: { requests: 100, window: "1m" },
perAgent: true, // Track limits per agent API key
},
},
});Policies
Policies enforce authorization rules on routes. They receive the full auth context and can make decisions based on user identity, role, agent permissions, or any other criteria.
import { definePolicy } from "@zauso-ai/capstan-core";
// Require any authenticated user
export const requireAuth = definePolicy({
key: "requireAuth",
title: "Require Authentication",
effect: "deny",
async check({ ctx }) {
if (!ctx.auth.isAuthenticated) {
return { effect: "deny", reason: "Authentication required" };
}
return { effect: "allow" };
},
});
// Require human approval for agent write actions
export const agentApproval = definePolicy({
key: "agentApproval",
title: "Agent Approval Required",
effect: "approve",
async check({ ctx }) {
if (ctx.auth.type === "agent") {
return {
effect: "approve",
reason: "Agent write actions require human approval",
};
}
return { effect: "allow" };
},
});Apply policies to routes via the policy field in defineAPI():
export const DELETE = defineAPI({
capability: "write",
resource: "ticket",
policy: "requireAuth",
async handler({ ctx }) {
// Only runs if policy allows
return { deleted: true };
},
});Permission Checking
The checkPermission() function evaluates whether a required permission is satisfied by a set of granted permissions. Permissions follow the resource:action pattern with wildcard support.
import { checkPermission } from "@zauso-ai/capstan-auth";
// Exact match
checkPermission({ resource: "ticket", action: "read" }, ["ticket:read"]);
// true
// Wildcard resource
checkPermission({ resource: "ticket", action: "write" }, ["*:write"]);
// true
// Wildcard action
checkPermission({ resource: "ticket", action: "delete" }, ["ticket:*"]);
// true
// Superuser
checkPermission({ resource: "ticket", action: "delete" }, ["*:*"]);
// trueCSRF Protection
Capstan uses the SameSite=Lax cookie attribute by default for session cookies. Additional CSRF guidance:
- Use
SameSite=Strictfor sensitive operations - Verify the
OriginorRefererheader in middleware - API key authentication (used by agents) is inherently CSRF-resistant since tokens are sent in the
Authorizationheader, not cookies
Configuration Example
Full auth configuration in capstan.config.ts:
import { defineConfig, env } from "@zauso-ai/capstan-core";
export default defineConfig({
app: { name: "my-app" },
auth: {
providers: [{ type: "apiKey" }],
session: {
strategy: "jwt",
secret: env("SESSION_SECRET") || crypto.randomUUID(),
maxAge: "7d",
},
},
});