UNPKG

@civic/auth-mcp

Version:

Civic Auth integration for MCP servers

989 lines (971 loc) 33.8 kB
// src/index.ts import { Router as Router2 } from "express"; // src/constants.ts var DEFAULT_WELLKNOWN_URL = "https://auth.civic.com/oauth/.well-known/openid-configuration"; var DEFAULT_SCOPES = ["openid", "profile", "email", "offline_access"]; var DEFAULT_CALLBACK_PORT = 8080; var DEFAULT_MCP_ROUTE = "/mcp"; var PUBLIC_CIVIC_CLIENT_ID = "12220cf4-1a9a-4964-8eb7-7c6d7d049f34"; // src/legacy/LegacyOAuthRouter.ts import { Router } from "express"; // src/resolveUrl.ts function resolveBaseUrl(req, options = {}) { let protocol; if (options.forceHttps) { protocol = "https"; } else if (options.protocolHeader) { const headerValue = req.headers?.[options.protocolHeader.toLowerCase()]; protocol = (typeof headerValue === "string" ? headerValue : void 0) ?? ("protocol" in req ? req.protocol : "http"); } else { protocol = "protocol" in req ? req.protocol : "http"; } let host; if (options.hostHeader) { const headerValue = req.headers?.[options.hostHeader.toLowerCase()]; host = typeof headerValue === "string" ? headerValue : void 0; } if (!host) { host = req.headers?.host ?? "localhost"; } return `${protocol}://${host}`; } // src/legacy/constants.ts var LEGACY_OAUTH_PATHS = { WELL_KNOWN: "/.well-known/oauth-authorization-server", AUTHORIZE: "/authorize", TOKEN: "/token", REGISTER: "/register" }; var OAUTH_ERRORS = { INVALID_REQUEST: "invalid_request", UNAUTHORIZED_CLIENT: "unauthorized_client", ACCESS_DENIED: "access_denied", UNSUPPORTED_RESPONSE_TYPE: "unsupported_response_type", INVALID_SCOPE: "invalid_scope", SERVER_ERROR: "server_error", TEMPORARILY_UNAVAILABLE: "temporarily_unavailable", INVALID_CLIENT: "invalid_client", INVALID_GRANT: "invalid_grant", UNSUPPORTED_GRANT_TYPE: "unsupported_grant_type" }; var STATE_EXPIRATION_MS = 10 * 60 * 1e3; var LEGACY_GRANT_TYPES = ["authorization_code", "refresh_token"]; var LEGACY_RESPONSE_TYPES = ["code"]; var LEGACY_TOKEN_AUTH_METHODS = ["client_secret_post", "client_secret_basic", "none"]; // src/legacy/OAuthProxyHandler.ts import { randomBytes } from "crypto"; // src/legacy/StateStore.ts var InMemoryStateStore = class { constructor() { this.states = /* @__PURE__ */ new Map(); } async set(key, state) { this.states.set(key, state); } async get(key) { const state = this.states.get(key); if (!state) return null; if (Date.now() - state.createdAt > STATE_EXPIRATION_MS) { this.states.delete(key); return null; } return state; } async delete(key) { this.states.delete(key); } async cleanup() { const now = Date.now(); for (const [key, state] of this.states.entries()) { if (now - state.createdAt > STATE_EXPIRATION_MS) { this.states.delete(key); } } } }; // src/legacy/OAuthProxyHandler.ts var DEFAULT_SCOPES2 = "openid email profile"; var ALLOWED_ADDITIONAL_SCOPES = ["mcp:tools"]; var OAuthProxyHandler = class { constructor(options, oidcConfig) { this.options = options; this.oidcConfig = oidcConfig; this.stateStore = options.stateStore || new InMemoryStateStore(); } /** * Handle authorization endpoint requests */ async handleAuthorize(req, res) { try { if (!req.url) { throw new Error("Request URL is missing"); } const url2 = new URL(req.url, `http://${req.headers.host}`); const params = url2.searchParams; const authRequest = { response_type: params.get("response_type") || "", client_id: params.get("client_id") || "", redirect_uri: params.get("redirect_uri") || "", state: params.get("state") || void 0, scope: params.get("scope") || DEFAULT_SCOPES2, // Do not permit missing scopes. code_challenge: params.get("code_challenge") || void 0, code_challenge_method: params.get("code_challenge_method") || void 0 }; if (!authRequest.response_type || !authRequest.client_id || !authRequest.redirect_uri) { return this.sendErrorRedirect(res, authRequest.redirect_uri, { error: OAUTH_ERRORS.INVALID_REQUEST, error_description: "Missing required parameters", state: authRequest.state }); } if (authRequest.response_type !== "code") { return this.sendErrorRedirect(res, authRequest.redirect_uri, { error: OAUTH_ERRORS.UNSUPPORTED_RESPONSE_TYPE, error_description: "Only 'code' response type is supported", state: authRequest.state }); } const internalState = this.generateState(); const stateData = { redirectUri: authRequest.redirect_uri, clientState: authRequest.state, codeChallenge: authRequest.code_challenge, codeChallengeMethod: authRequest.code_challenge_method, createdAt: Date.now(), scope: authRequest.scope, clientId: authRequest.client_id }; await this.stateStore.set(internalState, stateData); const authUrl = new URL(this.oidcConfig.authorization_endpoint); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("client_id", this.options.clientId || authRequest.client_id); authUrl.searchParams.set("redirect_uri", this.getMcpCallbackUrl(req)); authUrl.searchParams.set("state", internalState); if (authRequest.scope) { authUrl.searchParams.set("scope", authRequest.scope); } if (authRequest.code_challenge) { authUrl.searchParams.set("code_challenge", authRequest.code_challenge); if (authRequest.code_challenge_method) { authUrl.searchParams.set("code_challenge_method", authRequest.code_challenge_method); } } res.writeHead(302, { Location: authUrl.toString() }); res.end(); } catch (error) { console.error("Error handling authorize request:", error); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: OAUTH_ERRORS.SERVER_ERROR })); } } /** * Handle OAuth callback from auth server */ async handleCallback(req, res) { try { if (!req.url) { throw new Error("Request URL is missing"); } const url2 = new URL(req.url, `http://${req.headers.host}`); const params = url2.searchParams; const code = params.get("code"); const state = params.get("state"); const error = params.get("error"); if (!state) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: OAUTH_ERRORS.INVALID_REQUEST })); return; } const stateData = await this.stateStore.get(state); if (!stateData) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: OAUTH_ERRORS.INVALID_REQUEST, error_description: "Invalid state" })); return; } await this.stateStore.delete(state); if (error) { return this.sendErrorRedirect(res, stateData.redirectUri, { error, error_description: params.get("error_description") || void 0, error_uri: params.get("error_uri") || void 0, state: stateData.clientState }); } if (!code) { return this.sendErrorRedirect(res, stateData.redirectUri, { error: OAUTH_ERRORS.INVALID_REQUEST, error_description: "Missing authorization code", state: stateData.clientState }); } const redirectUrl = new URL(stateData.redirectUri); redirectUrl.searchParams.set("code", code); if (stateData.clientState) { redirectUrl.searchParams.set("state", stateData.clientState); } res.writeHead(302, { Location: redirectUrl.toString() }); res.end(); } catch (error) { console.error("Error handling callback:", error); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: OAUTH_ERRORS.SERVER_ERROR })); } } /** * Handle token endpoint requests */ async handleToken(req, res) { try { let tokenRequest; if ("body" in req && req.body) { tokenRequest = req.body; } else { const body = await this.parseRequestBody(req); tokenRequest = { grant_type: body.get("grant_type") || "", code: body.get("code") || void 0, redirect_uri: body.get("redirect_uri") || void 0, client_id: body.get("client_id") || void 0, client_secret: body.get("client_secret") || void 0, code_verifier: body.get("code_verifier") || void 0, refresh_token: body.get("refresh_token") || void 0, scope: body.get("scope") || void 0 }; } if (!tokenRequest.grant_type) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: OAUTH_ERRORS.INVALID_REQUEST })); return; } const tokenResponse = await fetch(this.oidcConfig.token_endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: tokenRequest.grant_type, ...tokenRequest.code && { code: tokenRequest.code }, ...tokenRequest.redirect_uri && { redirect_uri: this.getMcpCallbackUrl(req) }, ...tokenRequest.client_id && { client_id: this.options.clientId || tokenRequest.client_id }, ...tokenRequest.client_secret && { client_secret: tokenRequest.client_secret }, ...tokenRequest.code_verifier && { code_verifier: tokenRequest.code_verifier }, ...tokenRequest.refresh_token && { refresh_token: tokenRequest.refresh_token }, ...tokenRequest.scope && { scope: tokenRequest.scope } }).toString() }); const contentType = tokenResponse.headers.get("content-type") || ""; const responseBody = await tokenResponse.text(); res.writeHead(tokenResponse.status, { "Content-Type": contentType, "Cache-Control": "no-store", Pragma: "no-cache" }); res.end(responseBody); } catch (error) { console.error("Error handling token request:", error); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: OAUTH_ERRORS.SERVER_ERROR })); } } /** * Handle registration endpoint requests */ async handleRegistration(req, res) { try { if (!this.oidcConfig.registration_endpoint) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Registration not supported" })); return; } let bodyObj; if ("body" in req && req.body) { bodyObj = req.body; } else { const contentType = req.headers["content-type"] || ""; if (contentType.includes("application/json")) { const rawBody = await this.readRawBody(req); bodyObj = JSON.parse(rawBody); } else { const parsed = await this.parseRequestBody(req); bodyObj = Object.fromEntries(parsed); } } const requestedScopes = (bodyObj.scope || "").split(/\s+/).filter(Boolean); const additionalScopes = requestedScopes.filter((s) => ALLOWED_ADDITIONAL_SCOPES.includes(s)); const finalScope = [DEFAULT_SCOPES2, ...additionalScopes].join(" "); console.log(`Replacing requested scopes "${bodyObj.scope}" with "${finalScope}"`); bodyObj.scope = finalScope; const registrationResponse = await fetch(this.oidcConfig.registration_endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(bodyObj) }); const responseContentType = registrationResponse.headers.get("content-type") || ""; const responseBody = await registrationResponse.text(); res.writeHead(registrationResponse.status, { "Content-Type": responseContentType }); res.end(responseBody); } catch (error) { console.error("Error handling registration request:", error); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: OAUTH_ERRORS.SERVER_ERROR })); } } /** * Get the callback URL for the MCP server */ getMcpCallbackUrl(req) { const baseUrl = resolveBaseUrl(req, this.options); return `${baseUrl}/oauth/callback`; } /** * Generate a cryptographically secure state parameter */ generateState() { return randomBytes(32).toString("base64url"); } /** * Send an error redirect response */ sendErrorRedirect(res, redirectUri, error) { if (!redirectUri) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify(error)); return; } const url2 = new URL(redirectUri); url2.searchParams.set("error", error.error); if (error.error_description) { url2.searchParams.set("error_description", error.error_description); } if (error.error_uri) { url2.searchParams.set("error_uri", error.error_uri); } if (error.state) { url2.searchParams.set("state", error.state); } res.writeHead(302, { Location: url2.toString() }); res.end(); } /** * Parse request body from incoming request */ async parseRequestBody(req) { return new Promise((resolve, reject) => { let body = ""; req.on("data", (chunk) => { body += chunk.toString(); }); req.on("end", () => { try { resolve(new URLSearchParams(body)); } catch (error) { reject(error); } }); req.on("error", reject); }); } /** * Read raw body from request */ async readRawBody(req) { return new Promise((resolve, reject) => { let body = ""; req.on("data", (chunk) => { body += chunk.toString(); }); req.on("end", () => { resolve(body); }); req.on("error", reject); }); } }; // src/legacy/LegacyOAuthRouter.ts var LegacyOAuthRouter = class { constructor(options, oidcConfig) { this.options = options; this.oidcConfig = oidcConfig; this.oauthHandler = new OAuthProxyHandler(options, oidcConfig); } /** * Create and configure the legacy OAuth router */ createRouter() { const router = Router(); router.get(LEGACY_OAUTH_PATHS.WELL_KNOWN, (req, res) => { const baseUrl = resolveBaseUrl(req, this.options); const metadata = { issuer: baseUrl, authorization_endpoint: `${baseUrl}${LEGACY_OAUTH_PATHS.AUTHORIZE}`, token_endpoint: `${baseUrl}${LEGACY_OAUTH_PATHS.TOKEN}`, registration_endpoint: this.oidcConfig.registration_endpoint ? `${baseUrl}${LEGACY_OAUTH_PATHS.REGISTER}` : void 0, scopes_supported: this.options.scopesSupported || this.oidcConfig.scopes_supported || [], response_types_supported: LEGACY_RESPONSE_TYPES, grant_types_supported: LEGACY_GRANT_TYPES, token_endpoint_auth_methods_supported: LEGACY_TOKEN_AUTH_METHODS, code_challenge_methods_supported: ["S256", "plain"] }; res.json(metadata); }); router.get(LEGACY_OAUTH_PATHS.AUTHORIZE, async (req, res) => { await this.oauthHandler.handleAuthorize(req, res); }); router.get("/oauth/callback", async (req, res) => { await this.oauthHandler.handleCallback(req, res); }); router.post(LEGACY_OAUTH_PATHS.TOKEN, async (req, res) => { await this.oauthHandler.handleToken(req, res); }); if (this.oidcConfig.registration_endpoint) { router.post(LEGACY_OAUTH_PATHS.REGISTER, async (req, res) => { await this.oauthHandler.handleRegistration(req, res); }); } return router; } /** * Get the list of legacy OAuth paths for authentication bypass */ static getOAuthPaths() { return [ LEGACY_OAUTH_PATHS.WELL_KNOWN, LEGACY_OAUTH_PATHS.AUTHORIZE, LEGACY_OAUTH_PATHS.TOKEN, LEGACY_OAUTH_PATHS.REGISTER, "/oauth/callback" ]; } }; // src/McpServerAuth.ts import { createLocalJWKSet, createRemoteJWKSet, jwtVerify } from "jose"; // src/types.ts var AuthenticationError = class extends Error { }; var JWTVerificationError = class extends AuthenticationError { constructor(message, originalError) { super(message); this.originalError = originalError; this.name = "JWTVerificationError"; } }; // src/McpServerAuth.ts var getExpectedClientId = (options) => { if (options.clientId) { return options.clientId; } if (!options.wellKnownUrl || options.wellKnownUrl === DEFAULT_WELLKNOWN_URL) { return PUBLIC_CIVIC_CLIENT_ID; } return void 0; }; var getAuthServer = (options) => { if (options.wellKnownUrl && options.wellKnownUrl !== DEFAULT_WELLKNOWN_URL) return options.wellKnownUrl; if (options.allowDynamicClientRegistration) { const clientId = getExpectedClientId(options) ?? PUBLIC_CIVIC_CLIENT_ID; return DEFAULT_WELLKNOWN_URL.replace("/oauth/", `/oauth/${clientId}/`); } return DEFAULT_WELLKNOWN_URL; }; var verifyClientId = (payload, expectedClientId) => { if (!expectedClientId) { throw new AuthenticationError("Client ID verification is enabled but no expected client ID was provided"); } const clientIdMatches = payload.client_id === expectedClientId; const tidMatches = payload.tid === expectedClientId; if (!clientIdMatches && !tidMatches) { throw new AuthenticationError(`Invalid client_id or tid in token. Expected: ${expectedClientId}`); } }; var McpServerAuth = class _McpServerAuth { constructor(oidcConfig, options) { this.oidcConfig = oidcConfig; this.options = options; if (options.jwks) { this.jwks = createLocalJWKSet(options.jwks); } else { this.jwks = createRemoteJWKSet(new URL(oidcConfig.jwks_uri)); } } /** * Initialize the auth core by fetching OIDC configuration */ static async init(options = {}) { const wellKnownUrl = getAuthServer(options); console.log(`Fetching Civic Auth OIDC configuration from ${wellKnownUrl}`); const response = await fetch(wellKnownUrl); if (!response.ok) { throw new Error(`Failed to fetch Civic Auth configuration: ${response.statusText}`); } const oidcConfig = await response.json(); return new _McpServerAuth(oidcConfig, options); } /** * Get the OAuth Protected Resource metadata * @param resourceUrl The resource URL of the protected resource (e.g., https://my-server.com/mcp) */ getProtectedResourceMetadata(resourceUrl) { return { resource: resourceUrl, authorization_servers: [this.oidcConfig.issuer], scopes_supported: this.options.scopesSupported || DEFAULT_SCOPES, bearer_methods_supported: ["header"] }; } /** * Create auth info from a token (or null) and request * @param token The JWT token (can be null) * @param payload The JWT payload if token was already verified * @param request Optional request object to pass to onLogin callback * @returns ExtendedAuthInfo if successful, null otherwise */ async createAuthInfo(token, payload, request) { const inputAuthInfo = token && payload ? { token, clientId: payload.client_id || payload.aud, tenantId: payload.tid, scopes: payload.scope ? payload.scope.split(" ") : [], expiresAt: payload.exp, extra: { ...payload } } : null; if (!this.options.onLogin) return inputAuthInfo; return this.options.onLogin(inputAuthInfo, request); } /** * Extract and verify bearer token from authorization header * @param authHeader The Authorization header value * @returns Object with token and payload if valid, throws if invalid token, returns null values if no token */ async extractBearerToken(authHeader) { if (!authHeader?.startsWith("Bearer ")) { return { token: null, payload: null }; } const token = authHeader.substring(7); try { const { payload } = await jwtVerify(token, this.jwks, { issuer: this.oidcConfig.issuer }); if (!(this.options.disableClientIdVerification ?? false)) { verifyClientId(payload, getExpectedClientId(this.options)); } return { token, payload }; } catch (error) { throw new JWTVerificationError( error instanceof Error ? error.message : "JWT verification failed", error instanceof Error ? error : void 0 ); } } /** * Handle a request by extracting and verifying the bearer token * @param request The request object * @returns ExtendedAuthInfo if valid * @throws Error if authentication fails */ async handleRequest(request) { const { token, payload } = await this.extractBearerToken(request.headers.authorization); const authInfo = await this.createAuthInfo(token, payload, request); if (!authInfo) throw new AuthenticationError("Authentication failed"); return authInfo; } }; // src/client/CLIClient.ts import { Client } from "@modelcontextprotocol/sdk/client/index.js"; var CLIClient = class extends Client { /** * Connect to MCP server with automatic authentication handling * If the first connection fails due to auth, it will wait for the OAuth flow * to complete and then retry the connection */ async connect(transport) { try { await super.connect(transport); } catch (error) { if (error instanceof Error) { if (error.message === "Unauthorized") { console.log("Authorization required, waiting for user to complete OAuth flow..."); const authProvider = transport.authProvider; await authProvider.waitForAuthorizationCode(); console.log("Authorization completed."); return await super.connect(transport); } } throw error; } } }; // src/client/providers/persistence/InMemoryTokenPersistence.ts var InMemoryTokenPersistence = class { saveTokens(tokens) { this.tokens = tokens; } loadTokens() { return this.tokens; } clearTokens() { this.tokens = void 0; } }; // src/client/providers/CivicAuthProvider.ts var CivicAuthProvider = class { constructor(options) { this.clientSecret = options.clientSecret; this.tokenPersistence = options.tokenPersistence ?? new InMemoryTokenPersistence(); } saveTokens(tokens) { return this.tokenPersistence.saveTokens(tokens); } /** * Returns the stored tokens */ tokens() { return this.tokenPersistence.loadTokens(); } /** * Clears the stored tokens */ clearTokens() { return this.tokenPersistence.clearTokens(); } }; // src/client/providers/CLIAuthProvider.ts import { execFile } from "child_process"; import crypto from "crypto"; import http from "http"; import url from "url"; import { promisify } from "util"; import escapeHtml from "escape-html"; var CLIAuthProvider = class extends CivicAuthProvider { constructor(options) { super(options); this.clientId = options.clientId; this.scope = options.scope ?? DEFAULT_SCOPES.join(" "); this.callbackPort = options.callbackPort ?? DEFAULT_CALLBACK_PORT; this.enablePortFallback = options.enablePortFallback ?? true; this.authTimeoutMs = options.authTimeoutMs ?? 5 * 60 * 1e3; this.successHtml = options.successHtml ?? '<html lang="en"><body><h1>Authorization Successful</h1><p>You can now close this window.</p></body></html>'; this.errorHtml = options.errorHtml ?? '<html lang="en"><body><h1>Authorization Failed</h1><p>{{error}}</p></body></html>'; } clientInformation() { const info = { client_id: this.clientId }; if (this.clientSecret) { info.client_secret = this.clientSecret; } return info; } get clientMetadata() { return { redirect_uris: [this.getCallbackUrl(this.callbackPort)], client_name: this.clientId, scope: this.scope }; } codeVerifier() { if (!this.storedCodeVerifier) { this.storedCodeVerifier = crypto.randomBytes(32).toString("base64url"); } return this.storedCodeVerifier; } async redirectToAuthorization(authorizationUrl) { if (this.callbackServer) { throw new Error("Authorization flow already in progress. Please wait for it to complete."); } console.log(`Opening authorization URL in browser: ${authorizationUrl.href}`); const actualPort = await this.startCallbackServer(); let urlToOpen = authorizationUrl.href; if (actualPort) { this.callbackPort = actualPort; const authUrlObj = new URL(authorizationUrl); authUrlObj.searchParams.set("redirect_uri", this.getCallbackUrl(actualPort)); urlToOpen = authUrlObj.href; } await this.openInBrowser(urlToOpen); console.log("Please complete the authorization in your browser."); } /** * Registers the transport with the auth provider so that we can call finishAuth when the code is received. * @param transport */ registerTransport(transport) { this.transport = transport; } get redirectUrl() { return new URL(this.getCallbackUrl(this.callbackPort)); } saveCodeVerifier(codeVerifier) { this.storedCodeVerifier = codeVerifier; } getCallbackUrl(port) { return `http://localhost:${port}/callback`; } /** * Listen on Port Promise * @param server * @param port * @private port that is being listened on. */ listenOnPort(server, port) { return new Promise((resolve, reject) => { const onError = (err) => { server.off("listening", onListening); reject(err); }; const onListening = () => { server.off("error", onError); const address = server.address(); resolve(address.port); }; server.once("error", onError); server.once("listening", onListening); server.listen(port, "localhost"); }); } /** * Starts a local HTTP server to handle the OAuth callback with port fallback support * @returns The actual port number if different from the configured port, undefined otherwise */ async startCallbackServer() { this.authorizationCodePromise = new Promise((resolveCode, rejectCode) => { this.authorizationCodeResolve = resolveCode; this.authorizationCodeReject = rejectCode; }); this.callbackServer = http.createServer((req, res) => { try { if (!req.url) { res.writeHead(400); res.end("Bad Request"); return; } const parsedUrl = url.parse(req.url, true); if (parsedUrl.pathname === "/callback") { const code = parsedUrl.query.code; const error = parsedUrl.query.error; if (error) { res.writeHead(200, { "Content-Type": "text/html" }); res.end(this.errorHtml.replace("{{error}}", escapeHtml(error))); this.authorizationCodeReject?.(new Error(`OAuth error: ${error}`)); } else if (code) { res.writeHead(200, { "Content-Type": "text/html" }); res.end(this.successHtml); if (this.transport) { this.transport.finishAuth(code).then(() => this.authorizationCodeResolve?.(code)).catch((error2) => { console.error("Error in finishAuth:", error2); this.authorizationCodeReject?.(error2); }); } else { this.authorizationCodeReject?.(new Error("No transport registered")); } } else { res.writeHead(400); res.end("Missing authorization code"); } } else { res.writeHead(404); res.end("Not Found"); } } finally { this.cleanup(); } }); let actualPort; try { actualPort = await this.listenOnPort(this.callbackServer, this.callbackPort); } catch (err) { if (err.code === "EADDRINUSE" && this.enablePortFallback) { console.warn(`Port ${this.callbackPort} in use. Trying a random port...`); actualPort = await this.listenOnPort(this.callbackServer, 0); } else { throw err; } } this.serverTimeout = setTimeout(() => { console.warn(`OAuth callback server timeout reached after ${this.authTimeoutMs / 1e3}s. Closing server.`); this.cleanup(); }, this.authTimeoutMs); return actualPort !== this.callbackPort ? actualPort : void 0; } /** * Resets the instance to its post-initialization state * Stops any active server, clears timeouts */ cleanup() { if (this.callbackServer) { this.callbackServer.close(); this.callbackServer = void 0; } if (this.serverTimeout) { clearTimeout(this.serverTimeout); this.serverTimeout = void 0; } } /** * Waits for the authorization code from the callback */ async waitForAuthorizationCode() { if (!this.authorizationCodePromise) { throw new Error("Authorization flow not started"); } return this.authorizationCodePromise; } async openInBrowser(url2) { const execFileAsync = promisify(execFile); try { switch (process.platform) { case "darwin": await execFileAsync("open", [url2]); break; case "win32": await execFileAsync("cmd", ["/c", "start", url2]); break; default: await execFileAsync("xdg-open", [url2]); } } catch (error) { console.error("Failed to open browser:", error); console.log("Please open this URL manually:", url2); } } }; // src/client/providers/TokenAuthProvider.ts var TokenAuthProvider = class extends CivicAuthProvider { /** * Create a new TokenAuthProvider * @param tokenOrOptions - Either a token string or full options object */ constructor(tokenOrOptions) { const options = typeof tokenOrOptions === "string" ? { tokens: { access_token: tokenOrOptions, token_type: "Bearer" } } : tokenOrOptions; super(options); this.tokenPersistence.saveTokens(options.tokens); } get redirectUrl() { return ""; } get clientMetadata() { return { redirect_uris: [] }; } clientInformation() { return { client_id: "token-client" }; } redirectToAuthorization(_authorizationUrl) { } saveCodeVerifier(_codeVerifier) { } codeVerifier() { return ""; } }; // src/client/transport/RestartableStreamableHTTPClientTransport.ts import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; var RestartableStreamableHTTPClientTransport = class extends StreamableHTTPClientTransport { constructor(url2, opts) { super(url2, opts); this._cliAuthProvider = opts.authProvider; this._cliAuthProvider.registerTransport(this); } get authProvider() { return this._cliAuthProvider; } /** * Extends the start method to properly handle reconnection. * If the transport has already been started, it will disconnect first, * then start again to establish a fresh connection. */ async start() { try { await super.start(); } catch (_error) { } } async close() { } }; // src/index.ts async function auth(options = {}) { console.log(`Civic Auth MCP middleware initialized with options: ${JSON.stringify(options)}`); const enableLegacyOAuth = options.enableLegacyOAuth ?? true; const mcpServerAuth = await McpServerAuth.init(options); const mcpRoute = options.mcpRoute ?? DEFAULT_MCP_ROUTE; const oidcConfig = mcpServerAuth.oidcConfig; const router = Router2(); const wellKnownPath = "/.well-known/oauth-protected-resource"; router.use(wellKnownPath, (req, res) => { const mountPath = req.originalUrl.slice(0, req.originalUrl.indexOf(wellKnownPath)); const resourceUrl = `${resolveBaseUrl(req, options)}${mountPath}${mcpRoute}`; const metadata = mcpServerAuth.getProtectedResourceMetadata(resourceUrl); res.json(metadata); }); if (enableLegacyOAuth) { const legacyOAuthRouter = new LegacyOAuthRouter(options, oidcConfig); router.use(legacyOAuthRouter.createRouter()); } const tokenValidationMiddleware = async (req, res, next) => { if (req.path === "/.well-known/oauth-protected-resource") { return next(); } if (enableLegacyOAuth && LegacyOAuthRouter.getOAuthPaths().includes(req.path)) { return next(); } if (!req.path.startsWith(mcpRoute)) { return next(); } try { const authInfo = await mcpServerAuth.handleRequest(req); req.auth = authInfo; next(); } catch (error) { if (error instanceof AuthenticationError) { const baseUrl = resolveBaseUrl(req, options); const resourcePath = `${req.baseUrl}${mcpRoute}`; const metadataUrl = `${baseUrl}/.well-known/oauth-protected-resource${resourcePath}`; res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${metadataUrl}"`); res.status(401).json({ error: "authentication_error", error_description: error.message }); return; } res.status(500).json({ error: "internal_error", error_description: "An unexpected error occurred" }); return; } }; router.use(tokenValidationMiddleware); return router; } export { AuthenticationError, CLIAuthProvider, CLIClient, CivicAuthProvider, DEFAULT_CALLBACK_PORT, DEFAULT_MCP_ROUTE, DEFAULT_SCOPES, DEFAULT_WELLKNOWN_URL, InMemoryStateStore, InMemoryTokenPersistence, JWTVerificationError, McpServerAuth, PUBLIC_CIVIC_CLIENT_ID, RestartableStreamableHTTPClientTransport, TokenAuthProvider, auth, resolveBaseUrl }; //# sourceMappingURL=index.js.map