@keycardai/cloudflare
v0.4.0
Published
[Preview] Keycard auth integration for Cloudflare Workers — JWT verification, token exchange, and OAuth metadata
Downloads
34
Keywords
Readme
@keycardai/cloudflare
Preview. This package is pre-1.0. APIs may change between minor versions.
Keycard auth for Cloudflare Workers — bearer token verification, OAuth metadata endpoints, token exchange, and per-user token caching. No Express dependency.
This is the Workers equivalent of @keycardai/mcp's server-side middleware. If you're building an Express server, use @keycardai/mcp instead.
Installation
npm install @keycardai/cloudflare@keycardai/cloudflare depends on @keycardai/oauth (included automatically).
Quick Start
One-Line Worker Setup
The fastest way to add Keycard auth to a Worker:
import { createKeycardWorker } from "@keycardai/cloudflare";
export default createKeycardWorker({
// Bind accepted audiences to this Worker's public URL (its resource
// identifier in Keycard) so tokens minted for other resources are rejected.
audiences: ["https://my-worker.example.workers.dev"],
requiredScopes: ["read"],
scopesSupported: ["read", "write"],
resourceName: "My MCP Server",
fetch(request, env, ctx, auth) {
// auth is guaranteed — token is verified, scopes checked
return new Response(JSON.stringify({
message: `Hello ${auth.clientId}`,
scopes: auth.scopes,
}));
},
});createKeycardWorker() handles the full lifecycle: CORS preflight, /.well-known/* metadata endpoints, bearer token verification, then delegates to your handler.
Environment Variables
Configure in wrangler.toml or the Cloudflare dashboard:
[vars]
KEYCARD_ISSUER = "https://your-zone.keycard.cloud"
# Option A: Client credentials
KEYCARD_CLIENT_ID = "your-client-id"
KEYCARD_CLIENT_SECRET = "your-client-secret"
# Option B: Web identity (private_key_jwt — no secret needed)
# KEYCARD_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n..."Manual Setup
For more control, use the individual functions:
import {
verifyBearerToken,
isAuthError,
handleMetadataRequest,
} from "@keycardai/cloudflare";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// 1. Serve OAuth metadata
const metadata = await handleMetadataRequest(request, {
issuer: env.KEYCARD_ISSUER,
scopesSupported: ["read", "write"],
});
if (metadata) return metadata;
// 2. Verify bearer token. `issuers` pins the verifier to your zone so
// forged tokens from any other issuer are rejected before any JWKS lookup.
const auth = await verifyBearerToken(request, {
issuers: env.KEYCARD_ISSUER,
requiredScopes: ["read"],
});
if (isAuthError(auth)) return auth; // 401/403 Response
// 3. Use authenticated info
return new Response(`Hello ${auth.subject}`);
},
};Token Exchange with Caching
Exchange user tokens for upstream API tokens with per-user caching designed for Workers' shared-isolate model:
import {
createKeycardWorker,
IsolateSafeTokenCache,
resolveCredential,
} from "@keycardai/cloudflare";
import { TokenExchangeClient } from "@keycardai/oauth/tokenExchange";
let tokenCache: IsolateSafeTokenCache | undefined;
export default createKeycardWorker({
requiredScopes: ["read"],
async fetch(request, env, ctx, auth) {
// Lazy-init cache (module-level state is safe across requests in Workers)
if (!tokenCache) {
const credential = resolveCredential(env);
const client = new TokenExchangeClient(
env.KEYCARD_ISSUER,
credential.getAuth() ?? undefined,
);
tokenCache = new IsolateSafeTokenCache(client, { credential });
}
// The cache keys tokens per user, so require a subject: a token
// without one must not share another user's upstream token.
if (!auth.subject) {
return new Response("Token has no subject", { status: 401 });
}
// Exchange for upstream token (cached per-user, auto-refreshes)
const upstream = await tokenCache.getToken(
auth.subject,
auth.token,
env.KEYCARD_RESOURCE_URL!,
);
const resp = await fetch("https://api.github.com/user", {
headers: { Authorization: `Bearer ${upstream.accessToken}` },
});
return new Response(resp.body, resp);
},
});IsolateSafeTokenCache handles:
- Per-user keying (
sub::resource) for shared isolates - In-flight deduplication (concurrent requests share one exchange)
- Skew-aware TTL (refreshes before expiry)
- Bounded size with LRU-ish eviction
API
createKeycardWorker(options)
Returns an ExportedHandler with Keycard auth built in. Auto-detects credential type from env.
Set audiences to the Worker's public URL so bearer tokens minted for other resources on the same issuer are rejected.
verifyBearerToken(request, options?)
Verifies a Bearer token. Returns AuthInfo on success or a Response (401/403) on failure.
handleMetadataRequest(request, options)
Serves /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server. Returns null for non-metadata paths.
IsolateSafeTokenCache
Per-user token exchange cache for Workers. See above for usage.
resolveCredential(env)
Resolves WorkersClientSecret or WorkersWebIdentity from env bindings.
AuthInfo
interface AuthInfo {
token: string; // Raw bearer token
clientId: string; // client_id claim
scopes: string[]; // Granted scopes
expiresAt?: number; // Expiration (Unix seconds)
resource?: URL; // Audience/resource URL
subject?: string; // JWT sub claim
}Credential Types
| Type | Env vars | Auth method |
|---|---|---|
| WorkersClientSecret | KEYCARD_CLIENT_ID + KEYCARD_CLIENT_SECRET | Basic auth |
| WorkersWebIdentity | KEYCARD_PRIVATE_KEY | Private key JWT (RFC 7523) |
