@civic/auth-mcp
Version:
Civic Auth integration for MCP servers
1,038 lines (1,019 loc) • 36.6 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
AuthenticationError: () => AuthenticationError,
CLIAuthProvider: () => CLIAuthProvider,
CLIClient: () => CLIClient,
CivicAuthProvider: () => CivicAuthProvider,
DEFAULT_CALLBACK_PORT: () => DEFAULT_CALLBACK_PORT,
DEFAULT_MCP_ROUTE: () => DEFAULT_MCP_ROUTE,
DEFAULT_SCOPES: () => DEFAULT_SCOPES,
DEFAULT_WELLKNOWN_URL: () => DEFAULT_WELLKNOWN_URL,
InMemoryStateStore: () => InMemoryStateStore,
InMemoryTokenPersistence: () => InMemoryTokenPersistence,
JWTVerificationError: () => JWTVerificationError,
McpServerAuth: () => McpServerAuth,
PUBLIC_CIVIC_CLIENT_ID: () => PUBLIC_CIVIC_CLIENT_ID,
RestartableStreamableHTTPClientTransport: () => RestartableStreamableHTTPClientTransport,
TokenAuthProvider: () => TokenAuthProvider,
auth: () => auth,
resolveBaseUrl: () => resolveBaseUrl
});
module.exports = __toCommonJS(index_exports);
var import_express2 = require("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
var import_express = require("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
var import_node_crypto = require("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 (0, import_node_crypto.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 = (0, import_express.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
var import_jose = require("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 = (0, import_jose.createLocalJWKSet)(options.jwks);
} else {
this.jwks = (0, import_jose.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 (0, import_jose.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
var import_client = require("@modelcontextprotocol/sdk/client/index.js");
var CLIClient = class extends import_client.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
var import_node_child_process = require("child_process");
var import_node_crypto2 = __toESM(require("crypto"), 1);
var import_node_http = __toESM(require("http"), 1);
var import_node_url = __toESM(require("url"), 1);
var import_node_util = require("util");
var import_escape_html = __toESM(require("escape-html"), 1);
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 = import_node_crypto2.default.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 = import_node_http.default.createServer((req, res) => {
try {
if (!req.url) {
res.writeHead(400);
res.end("Bad Request");
return;
}
const parsedUrl = import_node_url.default.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}}", (0, import_escape_html.default)(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 = (0, import_node_util.promisify)(import_node_child_process.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
var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
var RestartableStreamableHTTPClientTransport = class extends import_streamableHttp.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 = (0, import_express2.Router)();
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;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
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.cjs.map