mcp-connector
Version:
MCP Remote Proxy Server for Streamable HTTP with OAuth Support.
342 lines • 14.6 kB
JavaScript
import { createHash, randomBytes } from "crypto";
import { createServer } from "http";
import { Socket } from "net";
import { join } from "path";
import { existsSync, readFileSync, writeFileSync } from "fs";
import { OAuthDialogManager } from "../utils/dialog-manager.js";
import { OAuthProxyServer } from "../proxy/oauth-proxy-server.js";
import { createLogger, logger as DefaultLogger, } from "../utils/file-logger.js";
import { AUTH_CONSTANTS, ROOT_CONFIG, TransportClientClassMap, } from "../constants/constants.js";
import { TokenManager } from "./token-manager.js";
export class DefaultAuthProvider {
redirectUrl;
port = 0;
clientMetadata;
static oauthProxy;
baseUrl;
clientTransport;
dialogManager;
tokenManager;
logger;
constructor(server) {
// Use server name from global config if available, else fallback to hash
const baseUrl = new URL(server.url);
const serverName = server.name;
this.logger = serverName
? createLogger(serverName, TokenManager.hashUrl(baseUrl.toString()))
: DefaultLogger;
this.logger.debug("Initializing DefaultAuthProvider with base URL", baseUrl.toString());
this.baseUrl = baseUrl;
this.dialogManager = new OAuthDialogManager(baseUrl.toString());
this.tokenManager = new TokenManager(this.logger, baseUrl.toString());
}
static async create(server) {
const instance = new DefaultAuthProvider(server);
const filePath = instance.getClientInfoFilePath();
let usablePort;
const staticConfigPort = server.port;
if (staticConfigPort) {
const free = await DefaultAuthProvider.isAuthPortAvailable(staticConfigPort);
if (free) {
usablePort = staticConfigPort;
instance.logger.debug(`Reusing static port from config: ${usablePort}`);
}
else {
instance.logger.debug(`Port from config info (${staticConfigPort}) is not free, will use a random port`);
}
}
else if (existsSync(filePath)) {
try {
const data = JSON.parse(readFileSync(filePath, "utf-8"));
if (data.redirect_uris && data.redirect_uris[0]) {
const redirectUrlPort = this.getPortFromUrl(data.redirect_uris[0]);
const free = await DefaultAuthProvider.isAuthPortAvailable(redirectUrlPort);
if (free) {
usablePort = redirectUrlPort;
instance.logger.debug(`Reusing available port from client info: ${usablePort}`);
}
else {
instance.logger.debug(`Port from client info (${redirectUrlPort}) is not free, will use a random port`);
}
}
}
catch (e) {
instance.logger.warn("Failed to read client info for port reuse:", e);
}
}
if (typeof usablePort === "undefined") {
usablePort = await DefaultAuthProvider.getAuthAvailablePort();
}
instance.port = usablePort;
instance.redirectUrl = `http://localhost:${usablePort}/callback`;
instance.clientMetadata = {
client_name: "MCP Connector",
client_uri: "https://github.com/lokendra-ss-18283/mcp-connector",
redirect_uris: [instance.redirectUrl],
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
token_endpoint_auth_method: "none",
};
return instance;
}
clientInformation() {
this.logger.debug("Getting client information", this.clientMetadata);
try {
const filePath = this.getClientInfoFilePath();
if (existsSync(filePath)) {
const data = JSON.parse(readFileSync(filePath, "utf-8"));
// Check and update redirect_uris if needed
if (Array.isArray(data.redirect_uris) &&
!data.redirect_uris.includes(this.redirectUrl)) {
this.logger.debug("Updating client information redirect_uris to match current redirectUrl");
data.redirect_uris = [this.redirectUrl];
this.saveClientInformation(data);
}
this.logger.debug("Returning client information from file", data);
return data;
}
else {
this.logger.debug("No client information file found for", filePath);
return undefined;
}
}
catch (error) {
this.logger.warn("Failed to read client information file:", error);
return undefined;
}
}
saveClientInformation(clientInformation) {
try {
const filePath = this.getClientInfoFilePath();
writeFileSync(filePath, JSON.stringify(clientInformation, null, 2), "utf-8");
this.logger.debug("Saved client information to file", {
filePath,
client_id: clientInformation.client_id,
});
}
catch (error) {
this.logger.warn("Failed to save client information file:", error);
}
}
state() {
const state = randomBytes(16).toString("base64url");
// Store state globally with timestamp for verification
AUTH_CONSTANTS.globalStateStore.set(state, {
timestamp: Date.now(),
url: this.baseUrl.toString(),
});
this.logger.debug("Generated and stored OAuth state", { state });
return state;
}
tokens() {
const storedTokenData = this.tokenManager.getToken(this.baseUrl.toString());
this.logger.debug("STORED TOKEN :: ", storedTokenData == null ? null : JSON.stringify(storedTokenData));
if (storedTokenData &&
typeof storedTokenData.access_token === "string" &&
typeof storedTokenData.token_type === "string") {
const { createdAt, ...tokenWithoutCreatedAt } = storedTokenData;
return tokenWithoutCreatedAt;
}
return undefined;
}
saveTokens(tokens) {
this.logger.debug("Saving tokens", tokens);
const storedTokenData = this.tokenManager.getToken(this.baseUrl.toString());
if (storedTokenData &&
storedTokenData.refresh_token &&
tokens.refresh_token &&
storedTokenData.refresh_token === tokens.refresh_token) {
const proxyInstance = AUTH_CONSTANTS.proxyInstances.get(TokenManager.hashUrl(this.baseUrl.toString()));
proxyInstance?.validateRefTokenUnauth(this.tokenManager, this.baseUrl.toString());
if (proxyInstance?.refreshRetry === 0) {
this.tokenManager.saveToken(this.baseUrl.toString(), tokens);
}
}
else {
this.tokenManager.saveToken(this.baseUrl.toString(), tokens);
}
this.logger.debug("Tokens saved to TokenManager");
}
async redirectToAuthorization(authorizationUrl) {
this.logger.info("Redirect to authorization URL requested", {
authorizationUrl: authorizationUrl.toString(),
});
await this.dialogManager.showAuthDialog(authorizationUrl.toString());
this.startProxyServer();
this.logger.info(`Please visit the following URL to authorize the application:`);
this.logger.info(authorizationUrl.toString());
}
saveCodeVerifier(codeVerifier) {
this.logger.debug("Saving code verifier");
// Save in global map using URL hash
const urlHash = TokenManager.hashUrl(this.baseUrl.toString());
AUTH_CONSTANTS.codeVerifierStore.set(urlHash, codeVerifier);
}
codeVerifier() {
const urlHash = TokenManager.hashUrl(this.baseUrl.toString());
let verifier = AUTH_CONSTANTS.codeVerifierStore.get(urlHash);
if (!verifier) {
verifier = this.generatePKCECodeVerifier();
this.logger.debug("Generated new PKCE code verifier");
AUTH_CONSTANTS.codeVerifierStore.set(urlHash, verifier);
}
return verifier;
}
generatePKCECodeVerifier() {
const buffer = randomBytes(32);
return buffer.toString("base64url");
}
generateCodeChallenge(codeVerifier) {
const verifier = codeVerifier || this.codeVerifier();
// Create SHA256 hash of the code verifier
const hash = createHash("sha256").update(verifier, "ascii").digest();
// Encode as base64url
return hash.toString("base64url");
}
getCodeChallengeMethod() {
return "S256";
}
clearTokens() {
this.logger.debug("Clearing stored tokens");
// Clear tokens from TokenManager
this.tokenManager.removeToken(this.baseUrl.toString());
this.logger.debug("Tokens cleared from TokenManager");
}
// PKCE helper methods
generateAuthorizationUrl(authorizationEndpoint, clientId, state) {
this.logger.debug("Generating authorization URL");
const codeVerifier = this.codeVerifier();
const codeChallenge = this.generateCodeChallenge(codeVerifier);
const params = new URLSearchParams({
response_type: "code",
client_id: clientId,
redirect_uri: this.redirectUrl,
code_challenge: codeChallenge,
code_challenge_method: this.getCodeChallengeMethod(),
scope: "mcp", // Default scope
});
const authUrl = `${authorizationEndpoint}?${params.toString()}`;
this.logger.debug("Generated authorization URL with PKCE", {
endpoint: authorizationEndpoint,
clientId,
codeChallenge: codeChallenge.substring(0, 10) + "...",
state,
});
return authUrl;
}
generateState() {
return randomBytes(16).toString("base64url");
}
setClientTransport(transport) {
this.clientTransport = transport;
this.logger.debug("Client transport instance set in auth provider");
}
getClientTransport() {
return this.clientTransport;
}
getClientInfoFilePath() {
const urlHash = TokenManager.hashUrl(this.baseUrl.toString());
// Stores in ~/.mcp-connector/client_info_{hash}.json
return join(this.tokenManager.getStorePath(), `client_info_${urlHash}.json`);
}
async startProxyServer() {
if (!DefaultAuthProvider.oauthProxy) {
this.logger.debug("Starting OAuth proxy server");
DefaultAuthProvider.oauthProxy = new OAuthProxyServer({
port: this.port,
clientTransport: this.clientTransport,
logger: this.logger,
});
DefaultAuthProvider.oauthProxy.on("oauth-success", async ({ url }) => {
this.logger.info("OAuth successful, restarting MCP server...");
const serverConfig = ROOT_CONFIG.servers.get(TokenManager.hashUrl(url));
if (serverConfig) {
const clientClass = TransportClientClassMap[ROOT_CONFIG.transportType];
const clientInstance = new clientClass(serverConfig, ROOT_CONFIG.transportType);
await clientInstance.setup();
}
else {
this.logger.error("Couldn't find proxy server config. Exiting application. Please ensure your configuration file is present and valid.");
process.exit(1);
}
});
await DefaultAuthProvider.oauthProxy.ensureServerRunning(this.logger);
this.logger.info(`OAuth proxy server started on port 8080`);
this.logger.info(`OAuth callback server is running on http://localhost:8080`);
}
}
static getPortFromUrl(url) {
let urlObj;
try {
urlObj = typeof url === "string" ? new URL(url) : url;
if (urlObj.port) {
return Number(urlObj.port);
}
// Return default port based on protocol if not explicitly set
if (urlObj.protocol === "http:")
return 80;
if (urlObj.protocol === "https:")
return 443;
return 0;
}
catch {
return 0;
}
}
// Global state verification function
static verifyOAuthState(state, logger) {
if (!AUTH_CONSTANTS.globalStateStore.has(state)) {
logger.error("OAuth state verification failed: state not found", {
state,
});
return false;
}
const stateData = AUTH_CONSTANTS.globalStateStore.get(state);
const now = Date.now();
const maxAge = 300000; // 5 minutes
if (now - stateData.timestamp > maxAge) {
logger.error("OAuth state verification failed: state expired", {
state,
age: now - stateData.timestamp,
});
AUTH_CONSTANTS.globalStateStore.delete(state);
return false;
}
logger.debug("OAuth state verification successful", { state });
return true;
}
static getOAuthState(state) {
return AUTH_CONSTANTS.globalStateStore.get(state);
}
// Factory function to create auth provider instances
static async createAuthProvider(server, config) {
const provider = await DefaultAuthProvider.create(server);
if (config) {
Object.assign(provider, config);
}
return provider;
}
static isAuthPortAvailable = async (port) => {
return new Promise((resolve) => {
const tester = new Socket();
tester.once("error", () => resolve(true));
tester.once("connect", () => {
tester.destroy();
resolve(false);
});
tester.connect(port, "127.0.0.1");
});
};
static getAuthAvailablePort = async () => {
// Get available port with dummy server
return new Promise((resolve, reject) => {
const tempServer = createServer();
tempServer.listen(0, () => {
const { port } = tempServer.address();
tempServer.close(() => resolve(port));
});
tempServer.on("error", (err) => reject(err));
});
};
}
//# sourceMappingURL=auth-provider.js.map