// src/auth/helpers.ts function getAuthSession(session) { if (!session) { throw new Error("Session is not authenticated"); } return session; } function requireAll(...checks) { return (auth) => checks.every( (check) => typeof check === "function" ? check(auth) : check ); } function requireAny(...checks) { return (auth) => checks.some((check) => typeof check === "function" ? check(auth) : check); } function requireAuth(auth) { return auth !== void 0 && auth !== null; } function requireRole(...allowedRoles) { return (auth) => { if (!auth) return false; const role = auth.role; return typeof role === "string" && allowedRoles.includes(role); }; } function requireScopes(...requiredScopes) { return (auth) => { if (!auth) return false; const authScopes = auth.scopes; if (!authScopes) return false; const scopeSet = Array.isArray(authScopes) ? new Set(authScopes) : authScopes instanceof Set ? authScopes : /* @__PURE__ */ new Set(); return requiredScopes.every((scope) => scopeSet.has(scope)); }; } // src/auth/OAuthProxy.ts import { randomBytes as randomBytes4 } from "crypto"; import { z } from "zod"; // src/auth/types.ts var DEFAULT_ACCESS_TOKEN_TTL = 3600; var DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH = 31536e3; var DEFAULT_REFRESH_TOKEN_TTL = 2592e3; var DEFAULT_AUTHORIZATION_CODE_TTL = 300; var DEFAULT_TRANSACTION_TTL = 600; // src/auth/utils/claimsExtractor.ts var ClaimsExtractor = class { config; // Claims that MUST NOT be copied from upstream (protect proxy's JWT integrity) PROTECTED_CLAIMS = /* @__PURE__ */ new Set([ "aud", "client_id", "exp", "iat", "iss", "jti", "nbf" ]); constructor(config) { if (typeof config === "boolean") { config = config ? {} : { fromAccessToken: false, fromIdToken: false }; } this.config = { allowComplexClaims: config.allowComplexClaims || false, allowedClaims: config.allowedClaims, blockedClaims: config.blockedClaims || [], claimPrefix: config.claimPrefix !== void 0 ? config.claimPrefix : false, // Default: no prefix fromAccessToken: config.fromAccessToken !== false, // Default: true fromIdToken: config.fromIdToken !== false, // Default: true maxClaimValueSize: config.maxClaimValueSize || 2e3 }; } /** * Extract claims from a token (access token or ID token) */ async extract(token, tokenType) { if (tokenType === "access" && !this.config.fromAccessToken) { return null; } if (tokenType === "id" && !this.config.fromIdToken) { return null; } if (!this.isJWT(token)) { return null; } const payload = this.decodeJWTPayload(token); if (!payload) { return null; } const filtered = this.filterClaims(payload); return this.applyPrefix(filtered); } /** * Apply prefix to claim names (if configured) */ applyPrefix(claims) { const prefix = this.config.claimPrefix; if (prefix === false || prefix === "" || prefix === void 0) { return claims; } const result = {}; for (const [key, value] of Object.entries(claims)) { result[`${prefix}${key}`] = value; } return result; } /** * Decode JWT payload without signature verification * Safe because token came from trusted upstream via server-to-server exchange */ decodeJWTPayload(token) { try { const parts = token.split("."); if (parts.length !== 3) { return null; } const payload = Buffer.from(parts[1], "base64url").toString("utf-8"); return JSON.parse(payload); } catch (error) { console.warn(`Failed to decode JWT payload: ${error}`); return null; } } /** * Filter claims based on security rules */ filterClaims(claims) { const result = {}; for (const [key, value] of Object.entries(claims)) { if (this.PROTECTED_CLAIMS.has(key)) { continue; } if (this.config.blockedClaims?.includes(key)) { continue; } if (this.config.allowedClaims && !this.config.allowedClaims.includes(key)) { continue; } if (!this.isValidClaimValue(value)) { console.warn(`Skipping claim '${key}' due to invalid value`); continue; } result[key] = value; } return result; } /** * Check if a token is in JWT format */ isJWT(token) { return token.split(".").length === 3; } /** * Validate a claim value (type and size checks) */ isValidClaimValue(value) { if (value === null || value === void 0) { return false; } const type = typeof value; if (type === "string") { const maxSize = this.config.maxClaimValueSize ?? 2e3; return value.length <= maxSize; } if (type === "number" || type === "boolean") { return true; } if (Array.isArray(value) || type === "object") { if (!this.config.allowComplexClaims) { return false; } try { const stringified = JSON.stringify(value); const maxSize = this.config.maxClaimValueSize ?? 2e3; return stringified.length <= maxSize; } catch { return false; } } return false; } }; // src/auth/utils/consent.ts import { createHmac } from "crypto"; var ConsentManager = class { signingKey; constructor(signingKey) { this.signingKey = signingKey || this.generateDefaultKey(); } /** * Create HTTP response with consent screen */ createConsentResponse(transaction, provider) { const consentData = { clientName: "MCP Client", provider, scope: transaction.scope, timestamp: Date.now(), transactionId: transaction.id }; const html = this.generateConsentScreen(consentData); return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" }, status: 200 }); } /** * Generate HTML for consent screen */ generateConsentScreen(data) { const { clientName, provider, scope, transactionId } = data; return ` Authorization Request `.trim(); } /** * Sign consent data for cookie */ signConsentCookie(data) { const payload = JSON.stringify(data); const signature = this.sign(payload); return `${Buffer.from(payload).toString("base64")}.${signature}`; } /** * Validate and parse consent cookie */ validateConsentCookie(cookie) { try { const [payloadB64, signature] = cookie.split("."); if (!payloadB64 || !signature) { return null; } const payload = Buffer.from(payloadB64, "base64").toString("utf8"); const expectedSignature = this.sign(payload); if (signature !== expectedSignature) { return null; } const data = JSON.parse(payload); const age = Date.now() - data.timestamp; if (age > 5 * 60 * 1e3) { return null; } return data; } catch { return null; } } /** * Escape HTML to prevent XSS */ escapeHtml(text) { const map = { "'": "'", '"': """, "/": "/", "&": "&", "<": "<", ">": ">" }; return text.replace(/[&<>"'/]/g, (char) => map[char] || char); } /** * Format scope for display */ formatScope(scope) { const scopeMap = { email: "Access your email address", openid: "Verify your identity", profile: "View your basic profile information", "read:user": "Read your user information", "write:user": "Modify your user information" }; return scopeMap[scope] || scope.replace(/_/g, " ").replace(/:/g, " - "); } /** * Generate default signing key if none provided */ generateDefaultKey() { return `fastmcp-consent-${Date.now()}-${Math.random()}`; } /** * Sign a payload using HMAC-SHA256 */ sign(payload) { return createHmac("sha256", this.signingKey).update(payload).digest("hex"); } }; // src/auth/utils/jwtIssuer.ts import { createHmac as createHmac2, pbkdf2, randomBytes } from "crypto"; import { promisify } from "util"; var pbkdf2Async = promisify(pbkdf2); var JWTIssuer = class { accessTokenTtl; audience; issuer; refreshTokenTtl; signingKey; constructor(config) { this.issuer = config.issuer; this.audience = config.audience; this.accessTokenTtl = config.accessTokenTtl || DEFAULT_ACCESS_TOKEN_TTL; this.refreshTokenTtl = config.refreshTokenTtl || DEFAULT_REFRESH_TOKEN_TTL; this.signingKey = Buffer.from(config.signingKey); } /** * Derive a signing key from a secret * Uses PBKDF2 for key derivation */ static async deriveKey(secret, iterations = 1e5) { const salt = Buffer.from("fastmcp-oauth-proxy"); const key = await pbkdf2Async(secret, salt, iterations, 32, "sha256"); return key.toString("base64"); } /** * Issue an access token */ issueAccessToken(clientId, scope, additionalClaims, expiresIn) { const now = Math.floor(Date.now() / 1e3); const jti = this.generateJti(); const claims = { aud: this.audience, client_id: clientId, exp: now + (expiresIn ?? this.accessTokenTtl), iat: now, iss: this.issuer, jti, scope, // Merge additional claims (custom claims from upstream) ...additionalClaims || {} }; return this.signToken(claims); } /** * Issue a refresh token */ issueRefreshToken(clientId, scope, additionalClaims, expiresIn) { const now = Math.floor(Date.now() / 1e3); const jti = this.generateJti(); const claims = { aud: this.audience, client_id: clientId, exp: now + (expiresIn ?? this.refreshTokenTtl), iat: now, iss: this.issuer, jti, scope, // Merge additional claims (custom claims from upstream) ...additionalClaims || {} }; return this.signToken(claims); } /** * Validate a JWT token */ async verify(token) { try { const parts = token.split("."); if (parts.length !== 3) { return { error: "Invalid token format", valid: false }; } const [headerB64, payloadB64, signatureB64] = parts; const expectedSignature = this.sign(`${headerB64}.${payloadB64}`); if (signatureB64 !== expectedSignature) { return { error: "Invalid signature", valid: false }; } const claims = JSON.parse( Buffer.from(payloadB64, "base64url").toString("utf-8") ); const now = Math.floor(Date.now() / 1e3); if (claims.exp <= now) { return { claims, error: "Token expired", valid: false }; } if (claims.iss !== this.issuer) { return { claims, error: "Invalid issuer", valid: false }; } if (claims.aud !== this.audience) { return { claims, error: "Invalid audience", valid: false }; } return { claims, valid: true }; } catch (error) { return { error: error instanceof Error ? error.message : "Validation failed", valid: false }; } } /** * Generate unique JWT ID */ generateJti() { return randomBytes(16).toString("base64url"); } /** * Sign data with HMAC-SHA256 */ sign(data) { const hmac = createHmac2("sha256", this.signingKey); hmac.update(data); return hmac.digest("base64url"); } /** * Sign a JWT token */ signToken(claims) { const header = { alg: "HS256", typ: "JWT" }; const headerB64 = Buffer.from(JSON.stringify(header)).toString("base64url"); const payloadB64 = Buffer.from(JSON.stringify(claims)).toString( "base64url" ); const signature = this.sign(`${headerB64}.${payloadB64}`); return `${headerB64}.${payloadB64}.${signature}`; } }; // src/auth/utils/pkce.ts import { createHash, randomBytes as randomBytes2 } from "crypto"; var PKCEUtils = class _PKCEUtils { /** * Generate a code challenge from a verifier * @param verifier The code verifier * @param method Challenge method: 'S256' or 'plain' (default: 'S256') * @returns Base64URL-encoded challenge string */ static generateChallenge(verifier, method = "S256") { if (method === "plain") { return verifier; } if (method === "S256") { const hash = createHash("sha256"); hash.update(verifier); return _PKCEUtils.base64URLEncode(hash.digest()); } throw new Error(`Unsupported challenge method: ${method}`); } /** * Generate a complete PKCE pair (verifier + challenge) * @param method Challenge method: 'S256' or 'plain' (default: 'S256') * @returns Object containing verifier and challenge */ static generatePair(method = "S256") { const verifier = _PKCEUtils.generateVerifier(); const challenge = _PKCEUtils.generateChallenge(verifier, method); return { challenge, verifier }; } /** * Generate a cryptographically secure code verifier * @param length Length of verifier (43-128 characters, default: 128) * @returns Base64URL-encoded verifier string */ static generateVerifier(length = 128) { if (length < 43 || length > 128) { throw new Error("PKCE verifier length must be between 43 and 128"); } const byteLength = Math.ceil(length * 3 / 4); const randomBytesBuffer = randomBytes2(byteLength); return _PKCEUtils.base64URLEncode(randomBytesBuffer).slice(0, length); } /** * Validate a code verifier against a challenge * @param verifier The code verifier to validate * @param challenge The expected challenge * @param method The challenge method used * @returns True if verifier matches challenge */ static validateChallenge(verifier, challenge, method) { if (!verifier || !challenge) { return false; } if (method === "plain") { return verifier === challenge; } if (method === "S256") { const computedChallenge = _PKCEUtils.generateChallenge(verifier, "S256"); return computedChallenge === challenge; } return false; } /** * Encode a buffer as base64url (RFC 4648) * @param buffer Buffer to encode * @returns Base64URL-encoded string */ static base64URLEncode(buffer) { return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); } }; // src/auth/utils/tokenStore.ts import { createCipheriv, createDecipheriv, randomBytes as randomBytes3, scryptSync } from "crypto"; var EncryptedTokenStorage = class { algorithm = "aes-256-gcm"; backend; encryptionKey; constructor(backend, encryptionKey) { this.backend = backend; const salt = Buffer.from("fastmcp-oauth-proxy-salt"); this.encryptionKey = scryptSync(encryptionKey, salt, 32); } async cleanup() { await this.backend.cleanup(); } async delete(key) { await this.backend.delete(key); } async get(key) { const encrypted = await this.backend.get(key); if (!encrypted) { return null; } try { const decrypted = await this.decrypt( encrypted, this.encryptionKey ); return JSON.parse(decrypted); } catch (error) { console.error("Failed to decrypt value:", error); return null; } } async save(key, value, ttl) { const encrypted = await this.encrypt( JSON.stringify(value), this.encryptionKey ); await this.backend.save(key, encrypted, ttl); } async decrypt(ciphertext, key) { const parts = ciphertext.split(":"); if (parts.length !== 3) { throw new Error("Invalid encrypted data format"); } const [ivHex, authTagHex, encrypted] = parts; const iv = Buffer.from(ivHex, "hex"); const authTag = Buffer.from(authTagHex, "hex"); const decipher = createDecipheriv(this.algorithm, key, iv); decipher.setAuthTag( authTag ); let decrypted = decipher.update(encrypted, "hex", "utf8"); decrypted += decipher.final("utf8"); return decrypted; } async encrypt(plaintext, key) { const iv = randomBytes3(16); const cipher = createCipheriv(this.algorithm, key, iv); let encrypted = cipher.update(plaintext, "utf8", "hex"); encrypted += cipher.final("hex"); const authTag = cipher.getAuthTag(); return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`; } }; var MemoryTokenStorage = class { cleanupInterval = null; store = /* @__PURE__ */ new Map(); constructor(cleanupIntervalMs = 6e4) { this.cleanupInterval = setInterval( () => void this.cleanup(), cleanupIntervalMs ); } async cleanup() { const now = Date.now(); const keysToDelete = []; for (const [key, entry] of this.store.entries()) { if (entry.expiresAt < now) { keysToDelete.push(key); } } for (const key of keysToDelete) { this.store.delete(key); } } async delete(key) { this.store.delete(key); } /** * Destroy the storage and clear cleanup interval */ destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.store.clear(); } async get(key) { const entry = this.store.get(key); if (!entry) { return null; } if (entry.expiresAt < Date.now()) { this.store.delete(key); return null; } return entry.value; } async save(key, value, ttl) { const expiresAt = ttl ? Date.now() + ttl * 1e3 : Number.MAX_SAFE_INTEGER; this.store.set(key, { expiresAt, value }); } /** * Get the number of stored items */ size() { return this.store.size; } }; // src/auth/OAuthProxy.ts var OAuthProxy = class { claimsExtractor = null; cleanupInterval = null; clientCodes = /* @__PURE__ */ new Map(); config; consentManager; jwtIssuer; registeredClients = /* @__PURE__ */ new Map(); tokenStorage; transactions = /* @__PURE__ */ new Map(); constructor(config) { this.config = { allowedRedirectUriPatterns: ["https://*", "http://localhost:*"], authorizationCodeTtl: DEFAULT_AUTHORIZATION_CODE_TTL, consentRequired: true, enableTokenSwap: true, // Enabled by default for security redirectPath: "/oauth/callback", transactionTtl: DEFAULT_TRANSACTION_TTL, upstreamTokenEndpointAuthMethod: "client_secret_basic", ...config }; let storage = config.tokenStorage || new MemoryTokenStorage(); const isAlreadyEncrypted = storage.constructor.name === "EncryptedTokenStorage"; if (!isAlreadyEncrypted && config.encryptionKey !== false) { const encryptionKey = typeof config.encryptionKey === "string" ? config.encryptionKey : this.generateSigningKey(); storage = new EncryptedTokenStorage(storage, encryptionKey); } this.tokenStorage = storage; this.consentManager = new ConsentManager( config.consentSigningKey || this.generateSigningKey() ); if (this.config.enableTokenSwap) { const signingKey = this.config.jwtSigningKey || this.generateSigningKey(); this.jwtIssuer = new JWTIssuer({ audience: this.config.baseUrl, issuer: this.config.baseUrl, signingKey }); } const claimsConfig = config.customClaimsPassthrough !== void 0 ? config.customClaimsPassthrough : true; if (claimsConfig !== false) { this.claimsExtractor = new ClaimsExtractor(claimsConfig); } this.startCleanup(); } /** * OAuth authorization endpoint */ async authorize(params) { if (!params.client_id || !params.redirect_uri || !params.response_type) { throw new OAuthProxyError( "invalid_request", "Missing required parameters" ); } if (params.response_type !== "code") { throw new OAuthProxyError( "unsupported_response_type", "Only 'code' response type is supported" ); } if (params.code_challenge && !params.code_challenge_method) { throw new OAuthProxyError( "invalid_request", "code_challenge_method required when code_challenge is present" ); } const transaction = await this.createTransaction(params); if (this.config.consentRequired && !transaction.consentGiven) { return this.consentManager.createConsentResponse( transaction, this.getProviderName() ); } return this.redirectToUpstream(transaction); } /** * Stop cleanup interval and destroy resources */ destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.transactions.clear(); this.clientCodes.clear(); this.registeredClients.clear(); } /** * Token endpoint - exchange authorization code for tokens */ async exchangeAuthorizationCode(request) { if (request.grant_type !== "authorization_code") { throw new OAuthProxyError( "unsupported_grant_type", "Only authorization_code grant type is supported" ); } const clientCode = this.clientCodes.get(request.code); if (!clientCode) { throw new OAuthProxyError( "invalid_grant", "Invalid or expired authorization code" ); } if (clientCode.clientId !== request.client_id) { throw new OAuthProxyError("invalid_client", "Client ID mismatch"); } if (clientCode.codeChallenge) { if (!request.code_verifier) { throw new OAuthProxyError( "invalid_request", "code_verifier required for PKCE" ); } const valid = PKCEUtils.validateChallenge( request.code_verifier, clientCode.codeChallenge, clientCode.codeChallengeMethod ); if (!valid) { throw new OAuthProxyError("invalid_grant", "Invalid PKCE verifier"); } } if (clientCode.used) { throw new OAuthProxyError( "invalid_grant", "Authorization code already used" ); } clientCode.used = true; this.clientCodes.set(request.code, clientCode); if (this.config.enableTokenSwap && this.jwtIssuer) { return await this.issueSwappedTokens( clientCode.clientId, clientCode.upstreamTokens ); } else { const response = { access_token: clientCode.upstreamTokens.accessToken, expires_in: clientCode.upstreamTokens.expiresIn, token_type: clientCode.upstreamTokens.tokenType }; if (clientCode.upstreamTokens.refreshToken) { response.refresh_token = clientCode.upstreamTokens.refreshToken; } if (clientCode.upstreamTokens.idToken) { response.id_token = clientCode.upstreamTokens.idToken; } if (clientCode.upstreamTokens.scope.length > 0) { response.scope = clientCode.upstreamTokens.scope.join(" "); } return response; } } /** * Token endpoint - refresh access token */ async exchangeRefreshToken(request) { if (request.grant_type !== "refresh_token") { throw new OAuthProxyError( "unsupported_grant_type", "Only refresh_token grant type is supported" ); } if (this.config.enableTokenSwap && this.jwtIssuer) { return await this.handleSwapModeRefresh(request); } return await this.handlePassthroughRefresh(request); } /** * Get OAuth discovery metadata */ getAuthorizationServerMetadata() { return { authorizationEndpoint: `${this.config.baseUrl}/oauth/authorize`, codeChallengeMethodsSupported: ["S256", "plain"], grantTypesSupported: ["authorization_code", "refresh_token"], issuer: this.config.baseUrl, registrationEndpoint: `${this.config.baseUrl}/oauth/register`, responseTypesSupported: ["code"], scopesSupported: this.config.scopes || [], tokenEndpoint: `${this.config.baseUrl}/oauth/token`, tokenEndpointAuthMethodsSupported: [ "client_secret_basic", "client_secret_post" ] }; } /** * Handle OAuth callback from upstream provider */ async handleCallback(request) { const url = new URL(request.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); const error = url.searchParams.get("error"); if (error) { const errorDescription = url.searchParams.get("error_description"); throw new OAuthProxyError(error, errorDescription || void 0); } if (!code || !state) { throw new OAuthProxyError( "invalid_request", "Missing code or state parameter" ); } const transaction = this.transactions.get(state); if (!transaction) { throw new OAuthProxyError("invalid_request", "Invalid or expired state"); } const upstreamTokens = await this.exchangeUpstreamCode(code, transaction); const clientCode = this.generateAuthorizationCode( transaction, upstreamTokens ); this.transactions.delete(state); const redirectUrl = new URL(transaction.clientCallbackUrl); redirectUrl.searchParams.set("code", clientCode); redirectUrl.searchParams.set("state", transaction.state); return new Response(null, { headers: { Location: redirectUrl.toString() }, status: 302 }); } /** * Handle consent form submission */ async handleConsent(request) { const formData = await request.formData(); const transactionId = formData.get("transaction_id"); const action = formData.get("action"); if (!transactionId) { throw new OAuthProxyError("invalid_request", "Missing transaction_id"); } const transaction = this.transactions.get(transactionId); if (!transaction) { throw new OAuthProxyError( "invalid_request", "Invalid or expired transaction" ); } if (action === "deny") { this.transactions.delete(transactionId); const redirectUrl = new URL(transaction.clientCallbackUrl); redirectUrl.searchParams.set("error", "access_denied"); redirectUrl.searchParams.set( "error_description", "User denied authorization" ); redirectUrl.searchParams.set("state", transaction.state); return new Response(null, { headers: { Location: redirectUrl.toString() }, status: 302 }); } transaction.consentGiven = true; this.transactions.set(transactionId, transaction); return this.redirectToUpstream(transaction); } /** * Load upstream tokens from a FastMCP JWT */ async loadUpstreamTokens(fastmcpToken) { if (!this.jwtIssuer) { return null; } const result = await this.jwtIssuer.verify(fastmcpToken); if (!result.valid || !result.claims?.jti) { return null; } const mapping = await this.tokenStorage.get( `mapping:${result.claims.jti}` ); if (!mapping) { return null; } const upstreamTokens = await this.tokenStorage.get( `upstream:${mapping.upstreamTokenKey}` ); return upstreamTokens; } /** * RFC 7591 Dynamic Client Registration */ async registerClient(request) { if (!request.redirect_uris || request.redirect_uris.length === 0) { throw new OAuthProxyError( "invalid_client_metadata", "redirect_uris is required" ); } for (const uri of request.redirect_uris) { if (!this.validateRedirectUri(uri)) { throw new OAuthProxyError( "invalid_redirect_uri", `Invalid redirect URI: ${uri}` ); } } const clientId = this.config.upstreamClientId; const client = { callbackUrl: request.redirect_uris[0], clientId, clientSecret: this.config.upstreamClientSecret, metadata: { client_name: request.client_name, client_uri: request.client_uri, contacts: request.contacts, jwks: request.jwks, jwks_uri: request.jwks_uri, logo_uri: request.logo_uri, policy_uri: request.policy_uri, scope: request.scope, software_id: request.software_id, software_version: request.software_version, tos_uri: request.tos_uri }, registeredAt: /* @__PURE__ */ new Date() }; this.registeredClients.set(request.redirect_uris[0], client); const response = { client_id: clientId, client_id_issued_at: Math.floor(Date.now() / 1e3), // Echo back optional metadata client_name: request.client_name, client_secret: this.config.upstreamClientSecret, client_secret_expires_at: 0, // Never expires client_uri: request.client_uri, contacts: request.contacts, grant_types: request.grant_types || [ "authorization_code", "refresh_token" ], jwks: request.jwks, jwks_uri: request.jwks_uri, logo_uri: request.logo_uri, policy_uri: request.policy_uri, redirect_uris: request.redirect_uris, response_types: request.response_types || ["code"], scope: request.scope, software_id: request.software_id, software_version: request.software_version, token_endpoint_auth_method: request.token_endpoint_auth_method || "client_secret_basic", tos_uri: request.tos_uri }; return response; } /** * Calculate access token TTL from upstream tokens */ calculateAccessTokenTtl(upstreamTokens) { if (upstreamTokens.expiresIn > 0) { return upstreamTokens.expiresIn; } else if (this.config.accessTokenTtl) { return this.config.accessTokenTtl; } else if (upstreamTokens.refreshToken) { return DEFAULT_ACCESS_TOKEN_TTL; } else { return DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH; } } /** * Clean up expired transactions and codes */ cleanup() { const now = Date.now(); for (const [id, transaction] of this.transactions.entries()) { if (transaction.expiresAt.getTime() < now) { this.transactions.delete(id); } } for (const [code, clientCode] of this.clientCodes.entries()) { if (clientCode.expiresAt.getTime() < now) { this.clientCodes.delete(code); } } void this.tokenStorage.cleanup(); } /** * Create a new OAuth transaction */ async createTransaction(params) { const transactionId = this.generateId(); const proxyPkce = PKCEUtils.generatePair("S256"); const transaction = { clientCallbackUrl: params.redirect_uri, clientCodeChallenge: params.code_challenge || "", clientCodeChallengeMethod: params.code_challenge_method || "plain", clientId: params.client_id, createdAt: /* @__PURE__ */ new Date(), expiresAt: new Date( Date.now() + (this.config.transactionTtl || 600) * 1e3 ), id: transactionId, proxyCodeChallenge: proxyPkce.challenge, proxyCodeVerifier: proxyPkce.verifier, scope: params.scope ? params.scope.split(" ") : this.config.scopes || [], state: params.state || this.generateId() }; this.transactions.set(transactionId, transaction); return transaction; } /** * Exchange authorization code with upstream provider */ async exchangeUpstreamCode(code, transaction) { const useBasicAuth = this.config.upstreamTokenEndpointAuthMethod === "client_secret_basic"; const bodyParams = { code, code_verifier: transaction.proxyCodeVerifier, grant_type: "authorization_code", redirect_uri: `${this.config.baseUrl}${this.config.redirectPath}` }; if (!useBasicAuth) { bodyParams.client_id = this.config.upstreamClientId; bodyParams.client_secret = this.config.upstreamClientSecret; } const headers = { "Content-Type": "application/x-www-form-urlencoded" }; if (useBasicAuth) { headers["Authorization"] = this.getBasicAuthHeader(); } const tokenResponse = await fetch(this.config.upstreamTokenEndpoint, { body: new URLSearchParams(bodyParams), headers, method: "POST" }); if (!tokenResponse.ok) { const error = await tokenResponse.json(); throw new OAuthProxyError( error.error || "server_error", error.error_description ); } const tokens = await this.parseTokenResponse(tokenResponse); return { accessToken: tokens.access_token, expiresIn: tokens.expires_in || 3600, idToken: tokens.id_token, issuedAt: /* @__PURE__ */ new Date(), refreshExpiresIn: tokens.refresh_expires_in, refreshToken: tokens.refresh_token, scope: tokens.scope ? tokens.scope.split(" ") : transaction.scope, tokenType: tokens.token_type || "Bearer" }; } /** * Extract JTI from a JWT token */ async extractJti(token) { if (!this.jwtIssuer) { throw new Error("JWT issuer not initialized"); } const result = await this.jwtIssuer.verify(token); if (!result.valid || !result.claims?.jti) { throw new Error("Failed to extract JTI from token"); } return result.claims.jti; } /** * Extract custom claims from upstream tokens * Combines claims from access token and ID token (if present) */ async extractUpstreamClaims(upstreamTokens) { if (!this.claimsExtractor) { return null; } const allClaims = {}; const accessClaims = await this.claimsExtractor.extract( upstreamTokens.accessToken, "access" ); if (accessClaims) { Object.assign(allClaims, accessClaims); } if (upstreamTokens.idToken) { const idClaims = await this.claimsExtractor.extract( upstreamTokens.idToken, "id" ); if (idClaims) { for (const [key, value] of Object.entries(idClaims)) { if (!(key in allClaims)) { allClaims[key] = value; } } } } return Object.keys(allClaims).length > 0 ? allClaims : null; } /** * Generate authorization code for client */ generateAuthorizationCode(transaction, upstreamTokens) { const code = this.generateId(); const clientCode = { clientId: transaction.clientId, code, codeChallenge: transaction.clientCodeChallenge, codeChallengeMethod: transaction.clientCodeChallengeMethod, createdAt: /* @__PURE__ */ new Date(), expiresAt: new Date( Date.now() + (this.config.authorizationCodeTtl || 300) * 1e3 ), transactionId: transaction.id, upstreamTokens }; this.clientCodes.set(code, clientCode); return code; } /** * Generate secure random ID */ generateId() { return randomBytes4(32).toString("base64url"); } /** * Generate signing key for consent cookies */ generateSigningKey() { return randomBytes4(32).toString("hex"); } /** * Generate Basic auth header value for upstream token endpoint * Per RFC 6749 Section 2.3.1, credentials must be URL-encoded before base64 encoding */ getBasicAuthHeader() { const encodedClientId = encodeURIComponent(this.config.upstreamClientId); const encodedClientSecret = encodeURIComponent( this.config.upstreamClientSecret ); return `Basic ${Buffer.from(`${encodedClientId}:${encodedClientSecret}`).toString("base64")}`; } /** * Get provider name for display */ getProviderName() { const url = new URL(this.config.upstreamAuthorizationEndpoint); return url.hostname; } /** * Handle passthrough mode refresh - forward refresh token directly to upstream */ async handlePassthroughRefresh(request) { const useBasicAuth = this.config.upstreamTokenEndpointAuthMethod === "client_secret_basic"; const bodyParams = { grant_type: "refresh_token", refresh_token: request.refresh_token, ...request.scope && { scope: request.scope } }; if (!useBasicAuth) { bodyParams.client_id = this.config.upstreamClientId; bodyParams.client_secret = this.config.upstreamClientSecret; } const headers = { "Content-Type": "application/x-www-form-urlencoded" }; if (useBasicAuth) { headers["Authorization"] = this.getBasicAuthHeader(); } const tokenResponse = await fetch(this.config.upstreamTokenEndpoint, { body: new URLSearchParams(bodyParams), headers, method: "POST" }); if (!tokenResponse.ok) { const error = await tokenResponse.json(); throw new OAuthProxyError( error.error || "invalid_grant", error.error_description ); } const tokens = await this.parseTokenResponse(tokenResponse); return { access_token: tokens.access_token, expires_in: tokens.expires_in || 3600, id_token: tokens.id_token, refresh_token: tokens.refresh_token, scope: tokens.scope, token_type: tokens.token_type || "Bearer" }; } /** * Handle swap mode refresh - verify FastMCP JWT and issue new tokens */ async handleSwapModeRefresh(request) { if (!this.jwtIssuer) { throw new Error("JWT issuer not initialized"); } const verifyResult = await this.jwtIssuer.verify(request.refresh_token); if (!verifyResult.valid) { throw new OAuthProxyError( "invalid_grant", "Invalid or expired refresh token" ); } const jti = verifyResult.claims?.jti; if (!jti) { throw new OAuthProxyError("invalid_grant", "Refresh token missing JTI"); } const mapping = await this.tokenStorage.get(`mapping:${jti}`); if (!mapping) { throw new OAuthProxyError( "invalid_grant", "Refresh token already used or expired" ); } const upstreamTokens = await this.tokenStorage.get( `upstream:${mapping.upstreamTokenKey}` ); if (!upstreamTokens) { throw new OAuthProxyError( "invalid_grant", "Upstream tokens not found or expired" ); } if (!upstreamTokens.refreshToken) { throw new OAuthProxyError( "invalid_grant", "No upstream refresh token available" ); } const refreshedUpstreamTokens = await this.refreshUpstreamTokens( upstreamTokens.refreshToken, request.scope ); if (refreshedUpstreamTokens.scope.length === 0) { refreshedUpstreamTokens.scope = upstreamTokens.scope; } const refreshTokenTtl = refreshedUpstreamTokens.refreshExpiresIn ?? this.config.refreshTokenTtl ?? DEFAULT_REFRESH_TOKEN_TTL; const accessTokenTtl = this.calculateAccessTokenTtl( refreshedUpstreamTokens ); const upstreamStorageTtl = Math.max(accessTokenTtl, refreshTokenTtl, 1); await this.tokenStorage.save( `upstream:${mapping.upstreamTokenKey}`, refreshedUpstreamTokens, upstreamStorageTtl ); return await this.issueSwappedTokensForRefresh( mapping.clientId, refreshedUpstreamTokens, mapping.upstreamTokenKey, jti ); } /** * Issue swapped tokens (JWT pattern) * Issues short-lived FastMCP JWTs and stores upstream tokens securely */ async issueSwappedTokens(clientId, upstreamTokens) { if (!this.jwtIssuer) { throw new Error("JWT issuer not initialized"); } const customClaims = await this.extractUpstreamClaims(upstreamTokens); let accessTokenTtl; if (upstreamTokens.expiresIn > 0) { accessTokenTtl = upstreamTokens.expiresIn; } else if (this.config.accessTokenTtl) { accessTokenTtl = this.config.accessTokenTtl; } else if (upstreamTokens.refreshToken) { accessTokenTtl = DEFAULT_ACCESS_TOKEN_TTL; } else { accessTokenTtl = DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH; } const refreshTokenTtl = upstreamTokens.refreshToken ? upstreamTokens.refreshExpiresIn ?? this.config.refreshTokenTtl ?? DEFAULT_REFRESH_TOKEN_TTL : 0; const upstreamStorageTtl = Math.max(accessTokenTtl, refreshTokenTtl, 1); const upstreamTokenKey = this.generateId(); await this.tokenStorage.save( `upstream:${upstreamTokenKey}`, upstreamTokens, upstreamStorageTtl ); const accessToken = this.jwtIssuer.issueAccessToken( clientId, upstreamTokens.scope, customClaims || void 0, accessTokenTtl ); const accessJti = await this.extractJti(accessToken); await this.tokenStorage.save( `mapping:${accessJti}`, { clientId, createdAt: /* @__PURE__ */ new Date(), expiresAt: new Date(Date.now() + accessTokenTtl * 1e3), jti: accessJti, scope: upstreamTokens.scope, upstreamTokenKey }, accessTokenTtl ); const response = { access_token: accessToken, expires_in: accessTokenTtl, scope: upstreamTokens.scope.join(" "), token_type: "Bearer" }; if (upstreamTokens.refreshToken) { const refreshToken = this.jwtIssuer.issueRefreshToken( clientId, upstreamTokens.scope, customClaims || void 0, refreshTokenTtl ); const refreshJti = await this.extractJti(refreshToken); await this.tokenStorage.save( `mapping:${refreshJti}`, { clientId, createdAt: /* @__PURE__ */ new Date(), expiresAt: new Date(Date.now() + refreshTokenTtl * 1e3), jti: refreshJti, scope: upstreamTokens.scope, upstreamTokenKey }, refreshTokenTtl ); response.refresh_token = refreshToken; } return response; } /** * Issue swapped tokens for refresh flow */ async issueSwappedTokensForRefresh(clientId, upstreamTokens, upstreamTokenKey, oldJti) { if (!this.jwtIssuer) { throw new Error("JWT issuer not initialized"); } await this.tokenStorage.delete(`mapping:${oldJti}`); const customClaims = await this.extractUpstreamClaims(upstreamTokens); const accessTokenTtl = this.calculateAccessTokenTtl(upstreamTokens); const refreshTokenTtl = upstreamTokens.refreshToken ? upstreamTokens.refreshExpiresIn ?? this.config.refreshTokenTtl ?? DEFAULT_REFRESH_TOKEN_TTL : 0; const accessToken = this.jwtIssuer.issueAccessToken( clientId, upstreamTokens.scope, customClaims || void 0, accessTokenTtl ); const accessJti = await this.extractJti(accessToken); await this.tokenStorage.save( `mapping:${accessJti}`, { clientId, createdAt: /* @__PURE__ */ new Date(), expiresAt: new Date(Date.now() + accessTokenTtl * 1e3), jti: accessJti, scope: upstreamTokens.scope, upstreamTokenKey }, accessTokenTtl ); const response = { access_token: accessToken, expires_in: accessTokenTtl, scope: upstreamTokens.scope.join(" "), token_type: "Bearer" }; if (upstreamTokens.refreshToken) { const refreshToken = this.jwtIssuer.issueRefreshToken( clientId, upstreamTokens.scope, customClaims || void 0, refreshTokenTtl ); const refreshJti = await this.extractJti(refreshToken); await this.tokenStorage.save( `mapping:${refreshJti}`, { clientId, createdAt: /* @__PURE__ */ new Date(), expiresAt: new Date(Date.now() + refreshTokenTtl * 1e3), jti: refreshJti, scope: upstreamTokens.scope, upstreamTokenKey }, refreshTokenTtl ); response.refresh_token = refreshToken; } return response; } /** * Match URI against pattern (supports wildcards) */ matchesPattern(uri, pattern) { const regex = new RegExp( "^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$" ); return regex.test(uri); } /** * Parse token response that can be either JSON or URL-encoded * GitHub Apps return URL-encoded format, most providers return JSON */ async parseTokenResponse(response) { const contentType = (response.headers.get("content-type") || "").toLowerCase(); const tokenResponseSchema = z.object({ access_token: z.string().min(1, "access_token cannot be empty"), expires_in: z.coerce.number().int().positive().optional(), id_token: z.string().optional(), refresh_expires_in: z.coerce.number().int().positive().optional(), refresh_token: z.string().optional(), scope: z.string().optional(), token_type: z.string().optional() }); if (contentType.includes("application/x-www-form-urlencoded")) { const text = await response.text(); const params = new URLSearchParams(text); const rawData = { access_token: params.get("access_token") || "", expires_in: params.get("expires_in") ? parseInt(params.get("expires_in")) : void 0, id_token: params.get("id_token") || void 0, refresh_expires_in: params.get("refresh_expires_in") ? parseInt(params.get("refresh_expires_in")) : void 0, refresh_token: params.get("refresh_token") || void 0, scope: params.get("scope") || void 0, token_type: params.get("token_type") || void 0 }; return tokenResponseSchema.parse(rawData); } const rawJson = await response.json(); return tokenResponseSchema.parse(rawJson); } /** * Redirect to upstream OAuth provider */ redirectToUpstream(transaction) { const authUrl = new URL(this.config.upstreamAuthorizationEndpoint); authUrl.searchParams.set("client_id", this.config.upstreamClientId); authUrl.searchParams.set( "redirect_uri", `${this.config.baseUrl}${this.config.redirectPath}` ); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("state", transaction.id); if (transaction.scope.length > 0) { authUrl.searchParams.set("scope", transaction.scope.join(" ")); } if (!this.config.forwardPkce) { authUrl.searchParams.set( "code_challenge", transaction.proxyCodeChallenge ); authUrl.searchParams.set("code_challenge_method", "S256"); } return new Response(null, { headers: { Location: authUrl.toString() }, status: 302 }); } /** * Refresh upstream tokens with provider */ async refreshUpstreamTokens(upstreamRefreshToken, requestedScope) { const useBasicAuth = this.config.upstreamTokenEndpointAuthMethod === "client_secret_basic"; const bodyParams = { grant_type: "refresh_token", refresh_token: upstreamRefreshToken, ...requestedScope && { scope: requestedScope } }; if (!useBasicAuth) { bodyParams.client_id = this.config.upstreamClientId; bodyParams.client_secret = this.config.upstreamClientSecret; } const headers = { "Content-Type": "application/x-www-form-urlencoded" }; if (useBasicAuth) { headers["Authorization"] = this.getBasicAuthHeader(); } const tokenResponse = await fetch(this.config.upstreamTokenEndpoint, { body: new URLSearchParams(bodyParams), headers, method: "POST" }); if (!tokenResponse.ok) { const error = await tokenResponse.json(); throw new OAuthProxyError( error.error || "invalid_grant", error.error_description || "Upstream refresh failed" ); } const tokens = await this.parseTokenResponse(tokenResponse); return { accessToken: tokens.access_token, expiresIn: tokens.expires_in || 3600, idToken: tokens.id_token, issuedAt: /* @__PURE__ */ new Date(), refreshExpiresIn: tokens.refresh_expires_in, refreshToken: tokens.refresh_token || upstreamRefreshToken, scope: tokens.scope ? tokens.scope.split(" ") : [], tokenType: tokens.token_type || "Bearer" }; } /** * Start periodic cleanup of expired transactions and codes */ startCleanup() { this.cleanupInterval = setInterval(() => { this.cleanup(); }, 6e4); } /** * Validate redirect URI against allowed patterns */ validateRedirectUri(uri) { try { const url = new URL(uri); const patterns = this.config.allowedRedirectUriPatterns || []; for (const pattern of patterns) { if (this.matchesPattern(uri, pattern)) { return true; } } return url.protocol === "https:" || url.hostname === "localhost" || url.hostname === "127.0.0.1"; } catch { return false; } } }; var OAuthProxyError = class extends Error { constructor(code, description, statusCode = 400) { super(code); this.code = code; this.description = description; this.statusCode = statusCode; this.name = "OAuthProxyError"; } toJSON() { return { error: this.code, error_description: this.description }; } toResponse() { return new Response(JSON.stringify(this.toJSON()), { headers: { "Content-Type": "application/json" }, status: this.statusCode }); } }; // src/auth/providers/AuthProvider.ts var AuthProvider = class { config; /** * Get the proxy, creating it lazily if needed. */ get proxy() { if (!this._proxy) { this._proxy = this.createProxy(); } return this._proxy; } _proxy; constructor(config) { this.config = config; } /** * Authenticate function to be used by FastMCP. * Extracts Bearer token, validates it, and returns session with upstream access token. */ async authenticate(request) { if (!request) { return void 0; } const authHeader = request.headers?.authorization; if (!authHeader || !authHeader.startsWith("Bearer ")) { return void 0; } const token = authHeader.slice(7); const upstreamTokens = await this.proxy.loadUpstreamTokens(token); if (!upstreamTokens) { return void 0; } return this.createSession(upstreamTokens); } /** * Get the OAuth configuration object for FastMCP ServerOptions. */ getOAuthConfig() { return { authorizationServer: this.proxy.getAuthorizationServerMetadata(), enabled: true, protectedResource: { authorizationServers: [this.config.baseUrl], resource: this.config.baseUrl, scopesSupported: this.config.scopes ?? this.getDefaultScopes() }, proxy: this.proxy }; } /** * Get the OAuthProxy instance (for advanced use cases). */ getProxy() { return this.proxy; } /** * Create a session object from upstream tokens. * Override in subclasses to add provider-specific session data. */ createSession(upstreamTokens) { return { accessToken: upstreamTokens.accessToken, expiresAt: upstreamTokens.expiresIn ? Math.floor(Date.now() / 1e3) + upstreamTokens.expiresIn : void 0, idToken: upstreamTokens.idToken, refreshToken: upstreamTokens.refreshToken, scopes: upstreamTokens.scope }; } }; // src/auth/providers/AzureProvider.ts var AzureProvider = class extends AuthProvider { tenantId; constructor(config) { super(config); this.tenantId = config.tenantId ?? "common"; } createProxy() { return new OAuthProxy({ allowedRedirectUriPatterns: this.config.allowedRedirectUriPatterns ?? [ "http://localhost:*", "https://*" ], baseUrl: this.config.baseUrl, consentRequired: this.config.consentRequired ?? true, encryptionKey: this.config.encryptionKey, jwtSigningKey: this.config.jwtSigningKey, scopes: this.config.scopes ?? this.getDefaultScopes(), tokenStorage: this.config.tokenStorage, upstreamAuthorizationEndpoint: this.getAuthorizationEndpoint(), upstreamClientId: this.config.clientId, upstreamClientSecret: this.config.clientSecret, upstreamTokenEndpoint: this.getTokenEndpoint() }); } getAuthorizationEndpoint() { return `https://login.microsoftonline.com/${this.tenantId}/oauth2/v2.0/authorize`; } getDefaultScopes() { return ["openid", "profile", "email"]; } getTokenEndpoint() { return `https://login.microsoftonline.com/${this.tenantId}/oauth2/v2.0/token`; } }; // src/auth/providers/GitHubProvider.ts var GitHubProvider = class extends AuthProvider { constructor(config) { super(config); } createProxy() { return new OAuthProxy({ allowedRedirectUriPatterns: this.config.allowedRedirectUriPatterns ?? [ "http://localhost:*", "https://*" ], baseUrl: this.config.baseUrl, consentRequired: this.config.consentRequired ?? true, encryptionKey: this.config.encryptionKey, jwtSigningKey: this.config.jwtSigningKey, scopes: this.config.scopes ?? this.getDefaultScopes(), tokenStorage: this.config.tokenStorage, upstreamAuthorizationEndpoint: this.getAuthorizationEndpoint(), upstreamClientId: this.config.clientId, upstreamClientSecret: this.config.clientSecret, upstreamTokenEndpoint: this.getTokenEndpoint() }); } getAuthorizationEndpoint() { return "https://github.com/login/oauth/authorize"; } getDefaultScopes() { return ["read:user", "user:email"]; } getTokenEndpoint() { return "https://github.com/login/oauth/access_token"; } }; // src/auth/providers/GoogleProvider.ts var GoogleProvider = class extends AuthProvider { constructor(config) { super(config); } createProxy() { return new OAuthProxy({ allowedRedirectUriPatterns: this.config.allowedRedirectUriPatterns ?? [ "http://localhost:*", "https://*" ], baseUrl: this.config.baseUrl, consentRequired: this.config.consentRequired ?? true, encryptionKey: this.config.encryptionKey, jwtSigningKey: this.config.jwtSigningKey, scopes: this.config.scopes ?? this.getDefaultScopes(), tokenStorage: this.config.tokenStorage, upstreamAuthorizationEndpoint: this.getAuthorizationEndpoint(), upstreamClientId: this.config.clientId, upstreamClientSecret: this.config.clientSecret, upstreamTokenEndpoint: this.getTokenEndpoint() }); } getAuthorizationEndpoint() { return "https://accounts.google.com/o/oauth2/v2/auth"; } getDefaultScopes() { return ["openid", "profile", "email"]; } getTokenEndpoint() { return "https://oauth2.googleapis.com/token"; } }; // src/auth/providers/OAuthProvider.ts var OAuthProvider = class extends AuthProvider { genericConfig; constructor(config) { super(config); this.genericConfig = config; } createProxy() { return new OAuthProxy({ allowedRedirectUriPatterns: this.config.allowedRedirectUriPatterns ?? [ "http://localhost:*", "https://*" ], baseUrl: this.config.baseUrl, consentRequired: this.config.consentRequired ?? true, encryptionKey: this.config.encryptionKey, jwtSigningKey: this.config.jwtSigningKey, scopes: this.config.scopes ?? this.getDefaultScopes(), tokenStorage: this.config.tokenStorage, upstreamAuthorizationEndpoint: this.getAuthorizationEndpoint(), upstreamClientId: this.config.clientId, upstreamClientSecret: this.config.clientSecret, upstreamTokenEndpoint: this.getTokenEndpoint(), upstreamTokenEndpointAuthMethod: this.genericConfig.tokenEndpointAuthMethod ?? "client_secret_basic" }); } getAuthorizationEndpoint() { return this.genericConfig.authorizationEndpoint; } getDefaultScopes() { return ["openid"]; } getTokenEndpoint() { return this.genericConfig.tokenEndpoint; } }; // src/auth/utils/diskStore.ts import { mkdir, readdir, readFile, rm, stat, writeFile } from "fs/promises"; import { join } from "path"; var DiskStore = class { cleanupInterval = null; directory; fileExtension; constructor(options) { this.directory = options.directory; this.fileExtension = options.fileExtension || ".json"; void this.ensureDirectory(); const cleanupIntervalMs = options.cleanupIntervalMs || 6e4; this.cleanupInterval = setInterval(() => { void this.cleanup(); }, cleanupIntervalMs); } /** * Clean up expired entries */ async cleanup() { try { await this.ensureDirectory(); const files = await readdir(this.directory); const now = Date.now(); for (const file of files) { if (!file.endsWith(this.fileExtension)) { continue; } try { const filePath = join(this.directory, file); const content = await readFile(filePath, "utf-8"); const entry = JSON.parse(content); if (entry.expiresAt < now) { await rm(filePath); } } catch (error) { console.warn(`Failed to read/parse file ${file}, deleting:`, error); try { await rm(join(this.directory, file)); } catch { } } } } catch (error) { console.error("Cleanup failed:", error); } } /** * Delete a value */ async delete(key) { const filePath = this.getFilePath(key); try { await rm(filePath); } catch (error) { if (error.code !== "ENOENT") { console.error(`Failed to delete key ${key}:`, error); } } } /** * Destroy the storage and clear cleanup interval */ destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } } /** * Retrieve a value */ async get(key) { const filePath = this.getFilePath(key); try { const content = await readFile(filePath, "utf-8"); const entry = JSON.parse(content); if (entry.expiresAt < Date.now()) { await rm(filePath); return null; } return entry.value; } catch (error) { if (error.code === "ENOENT") { return null; } console.error(`Failed to read key ${key}:`, error); return null; } } /** * Save a value with optional TTL */ async save(key, value, ttl) { await this.ensureDirectory(); const filePath = this.getFilePath(key); const expiresAt = ttl ? Date.now() + ttl * 1e3 : Number.MAX_SAFE_INTEGER; const entry = { expiresAt, value }; try { await writeFile(filePath, JSON.stringify(entry, null, 2), "utf-8"); } catch (error) { console.error(`Failed to save key ${key}:`, error); throw error; } } /** * Get the number of stored items */ async size() { try { await this.ensureDirectory(); const files = await readdir(this.directory); return files.filter((f) => f.endsWith(this.fileExtension)).length; } catch { return 0; } } /** * Ensure storage directory exists */ async ensureDirectory() { try { const stats = await stat(this.directory); if (!stats.isDirectory()) { throw new Error(`Path ${this.directory} exists but is not a directory`); } } catch (error) { if (error.code === "ENOENT") { await mkdir(this.directory, { recursive: true }); } else { throw error; } } } /** * Get file path for a key */ getFilePath(key) { const sanitizedKey = key.replace(/[^a-zA-Z0-9_-]/g, "_"); return join(this.directory, `${sanitizedKey}${this.fileExtension}`); } }; // src/auth/utils/jwks.ts var JWKSVerifier = class { config; jose; joseLoaded = false; jwksCache; constructor(config) { this.config = { cacheDuration: 36e5, // 1 hour cooldownDuration: 3e4, // 30 seconds ...config, audience: config.audience || "", issuer: config.issuer || "" }; } /** * Get the JWKS URI being used */ getJwksUri() { return this.config.jwksUri; } /** * Refresh the JWKS cache * Useful if you need to force a key refresh */ async refreshKeys() { await this.loadJose(); this.jwksCache = this.jose.createRemoteJWKSet( new URL(this.config.jwksUri), { cacheMaxAge: this.config.cacheDuration, cooldownDuration: this.config.cooldownDuration } ); } /** * Verify a JWT token using JWKS * * @param token - The JWT token to verify * @returns Verification result with claims if valid * * @example * ```typescript * const result = await verifier.verify(token); * if (result.valid) { * console.log('User:', result.claims?.client_id); * } else { * console.error('Invalid token:', result.error); * } * ``` */ async verify(token) { try { await this.loadJose(); const verifyOptions = {}; if (this.config.audience) { verifyOptions.audience = this.config.audience; } if (this.config.issuer) { verifyOptions.issuer = this.config.issuer; } const { payload } = await this.jose.jwtVerify( token, this.jwksCache, verifyOptions ); const claims = { aud: payload.aud, client_id: payload.client_id || payload.sub, exp: payload.exp, iat: payload.iat, iss: payload.iss, jti: payload.jti || "", scope: this.parseScope(payload.scope), ...payload // Include all other claims }; return { claims, valid: true }; } catch (error) { return { error: error.message || "Token verification failed", valid: false }; } } /** * Lazy load the jose library * Only loads when verification is first attempted */ async loadJose() { if (this.joseLoaded) { return; } try { this.jose = await import("jose"); this.joseLoaded = true; this.jwksCache = this.jose.createRemoteJWKSet( new URL(this.config.jwksUri), { cacheMaxAge: this.config.cacheDuration, cooldownDuration: this.config.cooldownDuration } ); } catch (error) { throw new Error( `JWKS verification requires the 'jose' package. Install it with: npm install jose If you don't need JWKS support, use HS256 signing instead (default). Original error: ${error.message}` ); } } /** * Parse scope from token payload * Handles both string (space-separated) and array formats */ parseScope(scope) { if (!scope) { return []; } if (typeof scope === "string") { return scope.split(" ").filter(Boolean); } if (Array.isArray(scope)) { return scope; } return []; } }; export { getAuthSession, requireAll, requireAny, requireAuth, requireRole, requireScopes, DEFAULT_ACCESS_TOKEN_TTL, DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH, DEFAULT_REFRESH_TOKEN_TTL, DEFAULT_AUTHORIZATION_CODE_TTL, DEFAULT_TRANSACTION_TTL, ConsentManager, JWTIssuer, PKCEUtils, EncryptedTokenStorage, MemoryTokenStorage, OAuthProxy, OAuthProxyError, AuthProvider, AzureProvider, GitHubProvider, GoogleProvider, OAuthProvider, DiskStore, JWKSVerifier }; //# sourceMappingURL=chunk-H4VC4YTC.js.map