UNPKG

mcpresso-oauth-server

Version:

Production-ready OAuth 2.1 server implementation for Model Context Protocol (MCP) with PKCE support

1,248 lines (1,238 loc) 58.3 kB
"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, { DEFAULT_OAUTH_CONFIG: () => DEFAULT_OAUTH_CONFIG, MCPOAuthHttpServer: () => MCPOAuthHttpServer, MCPOAuthServer: () => MCPOAuthServer, MemoryStorage: () => MemoryStorage, createDemoClient: () => createDemoClient, createJwtToken: () => createJwtToken, createProductionOAuthServer: () => createProductionOAuthServer, createTokenResponse: () => createTokenResponse, generateAccessToken: () => generateAccessToken, generateAuthorizationCode: () => generateAuthorizationCode, generateCodeChallenge: () => generateCodeChallenge, generateCodeVerifier: () => generateCodeVerifier, generatePkcePair: () => generatePkcePair, generateRandomToken: () => generateRandomToken, generateRefreshToken: () => generateRefreshToken, getScopeIntersection: () => getScopeIntersection, getTokenExpirationSeconds: () => getTokenExpirationSeconds, isTokenExpired: () => isTokenExpired, isValidCodeChallenge: () => isValidCodeChallenge, isValidCodeVerifier: () => isValidCodeVerifier, joinScope: () => joinScope, parseScope: () => parseScope, registerOAuthEndpoints: () => registerOAuthEndpoints, validateScope: () => validateScope, verifyCodeChallenge: () => verifyCodeChallenge, verifyJwtToken: () => verifyJwtToken }); module.exports = __toCommonJS(index_exports); // src/oauth-server.ts var import_jose = require("jose"); var import_crypto = require("crypto"); var DEFAULTS = { supportedScopes: ["read", "write", "openid", "profile", "email"], supportedGrantTypes: ["authorization_code", "refresh_token", "client_credentials"], supportedResponseTypes: ["code"], supportedCodeChallengeMethods: ["S256", "plain"] }; function normalizeConfig(config) { return { ...config, supportedScopes: config.supportedScopes ?? DEFAULTS.supportedScopes, supportedGrantTypes: config.supportedGrantTypes ?? DEFAULTS.supportedGrantTypes, supportedResponseTypes: config.supportedResponseTypes ?? DEFAULTS.supportedResponseTypes, supportedCodeChallengeMethods: config.supportedCodeChallengeMethods ?? DEFAULTS.supportedCodeChallengeMethods }; } var MCPOAuthServer = class { config; storage; constructor(config, storage) { const baseServerUrl = config.serverUrl || config.issuer; const fullConfig = normalizeConfig({ ...config, serverUrl: baseServerUrl, authorizationEndpoint: config.authorizationEndpoint ?? config.issuer + "/authorize", tokenEndpoint: config.tokenEndpoint ?? config.issuer + "/token", userinfoEndpoint: config.userinfoEndpoint ?? config.issuer + "/userinfo", jwksEndpoint: config.jwksEndpoint ?? config.issuer + "/.well-known/jwks.json", introspectionEndpoint: config.introspectionEndpoint ?? config.issuer + "/introspect", revocationEndpoint: config.revocationEndpoint ?? config.issuer + "/revoke", requireResourceIndicator: config.requireResourceIndicator ?? false, requirePkce: config.requirePkce ?? false, allowRefreshTokens: config.allowRefreshTokens ?? false, allowDynamicClientRegistration: config.allowDynamicClientRegistration ?? false, accessTokenLifetime: config.accessTokenLifetime ?? 3600, refreshTokenLifetime: config.refreshTokenLifetime ?? 3600, authorizationCodeLifetime: config.authorizationCodeLifetime ?? 600, supportedGrantTypes: config.supportedGrantTypes ?? DEFAULTS.supportedGrantTypes, supportedResponseTypes: config.supportedResponseTypes ?? DEFAULTS.supportedResponseTypes, supportedScopes: config.supportedScopes ?? DEFAULTS.supportedScopes, supportedCodeChallengeMethods: config.supportedCodeChallengeMethods ?? DEFAULTS.supportedCodeChallengeMethods, jwtAlgorithm: config.jwtAlgorithm ?? "HS256", http: config.http }); this.config = fullConfig; this.storage = storage; } // ===== USER AUTHENTICATION ===== /** * Handles user authentication during the authorization flow. * This method should be called before generating authorization codes. */ async authenticateUserForAuthFlow(credentials, sessionData, context) { if (this.config.auth) { if (this.config.auth.getCurrentUser) { const currentUser = await this.config.auth.getCurrentUser(sessionData, context); if (currentUser) { return currentUser; } } if (credentials && this.config.auth.authenticateUser) { return await this.config.auth.authenticateUser(credentials, context); } } if (!this.config.auth && credentials?.username === "demo@example.com") { const demoUser = await this.storage.getUser("demo-user"); return demoUser; } return null; } /** * Renders the login page for user authentication. */ async renderLoginPage(context, error) { if (this.config.auth?.renderLoginPage) { const result = await this.config.auth.renderLoginPage(context, error); if (typeof result === "string") { return result; } } return this.generateDefaultLoginPage(context, error); } /** * Handles consent/authorization for authenticated users. */ async handleUserConsent(user, context) { if (this.config.auth?.renderConsentPage) { const result = await this.config.auth.renderConsentPage(user, context); if (typeof result === "boolean") { return result; } return true; } return true; } generateDefaultLoginPage(context, error) { const errorHtml = error ? `<div style="color: red; margin-bottom: 16px;">${error}</div>` : ""; return ` <!DOCTYPE html> <html> <head> <title>Login - OAuth Authorization</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> body { font-family: system-ui, sans-serif; max-width: 400px; margin: 50px auto; padding: 20px; } .form-group { margin-bottom: 16px; } label { display: block; margin-bottom: 4px; font-weight: bold; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; } button { background: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; } button:hover { background: #0056b3; } .client-info { background: #f8f9fa; padding: 16px; border-radius: 4px; margin-bottom: 20px; } </style> </head> <body> <div class="client-info"> <h3>Authorization Request</h3> <p><strong>Client:</strong> ${context.clientId}</p> <p><strong>Scope:</strong> ${context.scope || "Default"}</p> </div> ${errorHtml} <form method="POST" action="/authorize"> <input type="hidden" name="response_type" value="code"> <input type="hidden" name="client_id" value="${context.clientId}"> <input type="hidden" name="redirect_uri" value="${context.redirectUri}"> <input type="hidden" name="scope" value="${context.scope || ""}"> <input type="hidden" name="resource" value="${context.resource || ""}"> <div class="form-group"> <label for="username">Username or Email:</label> <input type="text" id="username" name="username" required> </div> <div class="form-group"> <label for="password">Password:</label> <input type="password" id="password" name="password" required> </div> <button type="submit">Login & Authorize</button> </form> </body> </html> `; } // ===== AUTHORIZATION ENDPOINT ===== async handleAuthorizationRequest(params, credentials, sessionData, requestContext) { try { if (this.config.requireResourceIndicator && !params.resource) { return { error: "invalid_request", error_description: "resource parameter is required" }; } if (!params.resource && !this.config.requireResourceIndicator) { params.resource = this.config.serverUrl; } if (this.config.requirePkce && !params.code_challenge) { return { error: "invalid_request", error_description: "code_challenge parameter is required" }; } const client = await this.storage.getClient(params.client_id); if (!client) { return { error: "invalid_client", error_description: "Client not found" }; } if (client.redirectUris.length > 0 && !client.redirectUris.some((base) => params.redirect_uri.startsWith(base))) { return { error: "invalid_request", error_description: "Invalid redirect URI" }; } if (!client.grantTypes.includes("authorization_code")) { return { error: "unauthorized_client", error_description: "Client not authorized for authorization_code grant" }; } if (params.scope && !this.validateScope(params.scope, client.scopes)) { return { error: "invalid_scope", error_description: "Invalid scope" }; } const authContext = { clientId: params.client_id, scope: params.scope, resource: params.resource, redirectUri: params.redirect_uri, ipAddress: requestContext?.ipAddress || "0.0.0.0", userAgent: requestContext?.userAgent }; const user = await this.authenticateUserForAuthFlow(credentials || null, sessionData, authContext); if (!user) { const loginPage = await this.renderLoginPage(authContext); return { loginPage }; } const hasConsent = await this.handleUserConsent(user, authContext); if (!hasConsent) { return { error: "access_denied", error_description: "User denied authorization" }; } const code = this.generateCode(); const expiresAt = new Date(Date.now() + this.config.authorizationCodeLifetime * 1e3); const authCode = { code, clientId: params.client_id, userId: user.id, // Now using the actual authenticated user ID redirectUri: params.redirect_uri, scope: params.scope || "read", resource: params.resource, codeChallenge: params.code_challenge, codeChallengeMethod: params.code_challenge_method, expiresAt, createdAt: /* @__PURE__ */ new Date() }; await this.storage.createAuthorizationCode(authCode); const redirectUrl = new URL(params.redirect_uri); redirectUrl.searchParams.set("code", code); if (params.state) { redirectUrl.searchParams.set("state", params.state); } return { redirectUrl: redirectUrl.toString() }; } catch (error) { console.error("Authorization request error:", error); return { error: "server_error", error_description: "Internal server error" }; } } // ===== TOKEN ENDPOINT ===== async handleTokenRequest(params) { try { if (this.config.requireResourceIndicator && !params.resource) { return { error: "invalid_request", error_description: "resource parameter is required" }; } if (!params.resource && !this.config.requireResourceIndicator) { params.resource = this.config.serverUrl; } switch (params.grant_type) { case "authorization_code": return this.handleAuthorizationCodeGrant(params); case "refresh_token": return this.handleRefreshTokenGrant(params); case "client_credentials": return this.handleClientCredentialsGrant(params); default: return { error: "unsupported_grant_type", error_description: "Grant type not supported" }; } } catch (error) { console.error("Token request error:", error); return { error: "server_error", error_description: "Internal server error" }; } } async handleAuthorizationCodeGrant(params) { if (!params.code) { return { error: "invalid_request", error_description: "code parameter is required" }; } if (this.config.requirePkce && !params.code_verifier) { return { error: "invalid_request", error_description: "code_verifier parameter is required" }; } const authCode = await this.storage.getAuthorizationCode(params.code); if (!authCode) { return { error: "invalid_grant", error_description: "Invalid authorization code" }; } if (authCode.expiresAt < /* @__PURE__ */ new Date()) { await this.storage.deleteAuthorizationCode(params.code); return { error: "invalid_grant", error_description: "Authorization code expired" }; } const client = await this.storage.getClient(authCode.clientId); if (!client) { return { error: "invalid_client", error_description: "Client not found" }; } if (authCode.codeChallenge) { if (!params.code_verifier) { return { error: "invalid_request", error_description: "code_verifier is required" }; } const expectedChallenge = authCode.codeChallengeMethod === "S256" ? this.generateCodeChallenge(params.code_verifier) : params.code_verifier; if (authCode.codeChallenge !== expectedChallenge) { return { error: "invalid_grant", error_description: "Invalid code verifier" }; } } if (params.redirect_uri && params.redirect_uri !== authCode.redirectUri) { return { error: "invalid_grant", error_description: "Redirect URI mismatch" }; } await this.storage.deleteAuthorizationCode(params.code); const audience = params.resource || authCode.resource || this.config.serverUrl; const accessToken = await this.generateAccessToken(client.id, authCode.userId, authCode.scope, audience); let refreshToken; if (this.config.allowRefreshTokens) { refreshToken = await this.generateRefreshToken(accessToken.token, client.id, authCode.userId, authCode.scope, audience); } return { access_token: accessToken.token, token_type: "Bearer", expires_in: this.config.accessTokenLifetime, refresh_token: refreshToken, scope: authCode.scope }; } async handleRefreshTokenGrant(params) { if (!params.refresh_token) { return { error: "invalid_request", error_description: "refresh_token parameter is required" }; } const refreshToken = await this.storage.getRefreshToken(params.refresh_token); if (!refreshToken) { return { error: "invalid_grant", error_description: "Invalid refresh token" }; } if (refreshToken.expiresAt < /* @__PURE__ */ new Date()) { await this.storage.deleteRefreshToken(params.refresh_token); return { error: "invalid_grant", error_description: "Refresh token expired" }; } const client = await this.storage.getClient(refreshToken.clientId); if (!client) { return { error: "invalid_client", error_description: "Client not found" }; } if (params.resource && refreshToken.audience && params.resource !== refreshToken.audience) { return { error: "invalid_grant", error_description: "Resource indicator mismatch" }; } await this.storage.deleteRefreshToken(params.refresh_token); await this.storage.deleteRefreshTokensByAccessToken(refreshToken.accessTokenId); const accessToken = await this.generateAccessToken(client.id, refreshToken.userId, refreshToken.scope, params.resource); let newRefreshToken; if (this.config.allowRefreshTokens) { newRefreshToken = await this.generateRefreshToken(accessToken.token, client.id, refreshToken.userId, refreshToken.scope, params.resource); } return { access_token: accessToken.token, token_type: "Bearer", expires_in: this.config.accessTokenLifetime, refresh_token: newRefreshToken, scope: refreshToken.scope }; } async handleClientCredentialsGrant(params) { const client = await this.storage.getClient(params.client_id); if (!client) { return { error: "invalid_client", error_description: "Client not found" }; } if (client.type === "confidential" && client.secret !== params.client_secret) { return { error: "invalid_client", error_description: "Invalid client secret" }; } if (!client.grantTypes.includes("client_credentials")) { return { error: "unauthorized_client", error_description: "Client not authorized for client_credentials grant" }; } const accessToken = await this.generateAccessToken(client.id, void 0, params.scope || "read", params.resource); return { access_token: accessToken.token, token_type: "Bearer", expires_in: this.config.accessTokenLifetime, scope: accessToken.scope }; } // ===== TOKEN INTROSPECTION ===== async introspectToken(token) { try { const accessToken = await this.storage.getAccessToken(token); if (!accessToken || accessToken.expiresAt < /* @__PURE__ */ new Date()) { return { active: false }; } const client = await this.storage.getClient(accessToken.clientId); const user = accessToken.userId ? await this.storage.getUser(accessToken.userId) : void 0; return { active: true, scope: accessToken.scope, client_id: accessToken.clientId, username: user?.username, exp: Math.floor(accessToken.expiresAt.getTime() / 1e3), aud: accessToken.audience // MCP-specific: audience for validation }; } catch (error) { console.error("Token introspection error:", error); return { active: false }; } } // ===== TOKEN REVOCATION ===== async revokeToken(token, clientId) { try { const accessToken = await this.storage.getAccessToken(token); if (!accessToken || accessToken.clientId !== clientId) { return { success: false }; } await this.storage.deleteAccessToken(token); await this.storage.deleteRefreshTokensByAccessToken(token); return { success: true }; } catch (error) { console.error("Token revocation error:", error); return { success: false }; } } // ===== USER INFO ===== async getUserInfo(token) { try { const accessToken = await this.storage.getAccessToken(token); if (!accessToken || accessToken.expiresAt < /* @__PURE__ */ new Date()) { return { error: "invalid_token", error_description: "Invalid or expired token" }; } if (!accessToken.userId) { return { error: "invalid_token", error_description: "Token does not contain user information" }; } const user = await this.storage.getUser(accessToken.userId); if (!user) { return { error: "invalid_token", error_description: "User not found" }; } return { sub: user.id, name: user.username, email: user.email, scope: accessToken.scope }; } catch (error) { console.error("User info error:", error); return { error: "server_error", error_description: "Internal server error" }; } } // ===== DYNAMIC CLIENT REGISTRATION (RFC 7591) ===== async registerClient(request) { try { if (!this.config.allowDynamicClientRegistration) { return { error: "access_denied", error_description: "Dynamic client registration is not supported" }; } if (!request.redirect_uris || request.redirect_uris.length === 0) { return { error: "invalid_redirect_uri", error_description: "redirect_uris is required" }; } const clientId = this.generateClientId(); const clientSecret = this.generateClientSecret(); const client = { id: clientId, secret: clientSecret, name: request.client_name || `Dynamic Client ${clientId}`, type: "confidential", // Default to confidential for dynamic registration redirectUris: request.redirect_uris, scopes: request.scope ? request.scope.split(" ") : ["read"], grantTypes: request.grant_types || ["authorization_code"], createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }; await this.storage.createClient(client); const response = { client_id: clientId, client_secret: clientSecret, client_id_issued_at: Math.floor(Date.now() / 1e3), client_secret_expires_at: 0, // No expiration for now redirect_uris: request.redirect_uris, client_name: request.client_name, client_uri: request.client_uri, logo_uri: request.logo_uri, scope: request.scope, grant_types: request.grant_types, response_types: request.response_types, token_endpoint_auth_method: request.token_endpoint_auth_method, token_endpoint_auth_signing_alg: request.token_endpoint_auth_signing_alg, contacts: request.contacts, policy_uri: request.policy_uri, terms_of_service_uri: request.terms_of_service_uri, jwks_uri: request.jwks_uri, jwks: request.jwks, software_id: request.software_id, software_version: request.software_version }; return response; } catch (error) { console.error("Client registration error:", error); return { error: "server_error", error_description: "Internal server error" }; } } // ===== MCP METADATA ENDPOINTS ===== getProtectedResourceMetadata() { return { resource: this.config.serverUrl, authorization_servers: [this.config.issuer], scopes_supported: [...this.config.supportedScopes], bearer_methods_supported: ["Authorization header"] }; } getAuthorizationServerMetadata() { return { issuer: this.config.issuer, authorization_endpoint: this.config.authorizationEndpoint, token_endpoint: this.config.tokenEndpoint, userinfo_endpoint: this.config.userinfoEndpoint, jwks_uri: this.config.jwksEndpoint, revocation_endpoint: this.config.revocationEndpoint, introspection_endpoint: this.config.introspectionEndpoint, registration_endpoint: this.config.allowDynamicClientRegistration ? `${this.config.issuer}/register` : void 0, grant_types_supported: [...this.config.supportedGrantTypes], response_types_supported: [...this.config.supportedResponseTypes], scopes_supported: [...this.config.supportedScopes], token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post", "none"], code_challenge_methods_supported: [...this.config.supportedCodeChallengeMethods], resource_indicators_supported: this.config.requireResourceIndicator }; } // ===== UTILITY METHODS ===== generateCode() { return (0, import_crypto.randomBytes)(32).toString("base64url"); } generateCodeChallenge(verifier) { return (0, import_crypto.createHash)("sha256").update(verifier).digest("base64url"); } generateClientId() { return `client_${(0, import_crypto.randomBytes)(16).toString("hex")}`; } generateClientSecret() { return (0, import_crypto.randomBytes)(32).toString("base64url"); } async generateAccessToken(clientId, userId, scope, audience) { const now = Math.floor(Date.now() / 1e3); const payload = { iss: this.config.issuer, sub: userId || clientId, aud: audience || this.config.serverUrl, iat: now, exp: now + this.config.accessTokenLifetime, scope: scope || "read", client_id: clientId }; const secret = new TextEncoder().encode(this.config.jwtSecret); const jwt = await new import_jose.SignJWT(payload).setProtectedHeader({ alg: this.config.jwtAlgorithm || "HS256" }).sign(secret); const accessToken = { token: jwt, clientId, userId, scope: payload.scope, expiresAt: new Date((now + this.config.accessTokenLifetime) * 1e3), createdAt: /* @__PURE__ */ new Date(), audience: payload.aud }; await this.storage.createAccessToken(accessToken); return accessToken; } async generateRefreshToken(accessTokenId, clientId, userId, scope, audience) { const token = (0, import_crypto.randomBytes)(32).toString("base64url"); const expiresAt = new Date(Date.now() + this.config.refreshTokenLifetime * 1e3); const refreshToken = { token, accessTokenId, clientId, userId, scope: scope || "read", expiresAt, createdAt: /* @__PURE__ */ new Date(), audience // MCP-specific: store audience for validation }; await this.storage.createRefreshToken(refreshToken); return token; } validateScope(requestedScope, allowedScopes) { const requestedScopes = requestedScope.split(" "); return requestedScopes.every((scope) => allowedScopes.includes(scope)); } // ===== CLEANUP ===== async cleanup() { await Promise.all([ this.storage.cleanupExpiredCodes(), this.storage.cleanupExpiredTokens(), this.storage.cleanupExpiredRefreshTokens() ]); } // ===== ADMIN METHODS ===== async getStats() { return this.storage.getStats(); } // Public methods for admin access async listClients() { return this.storage.listClients(); } async listUsers() { return this.storage.listUsers(); } }; // src/http-server.ts var import_express = __toESM(require("express"), 1); var import_cors = __toESM(require("cors"), 1); var import_compression = __toESM(require("compression"), 1); var import_helmet = __toESM(require("helmet"), 1); var import_express_rate_limit = __toESM(require("express-rate-limit"), 1); function registerOAuthEndpoints(app, oauthServer, basePath = "") { const renderSuccessPage = (redirectUrl) => `<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Authorization successful</title> <meta http-equiv="refresh" content="1;url=${redirectUrl}"> <style> :root { --primary:#2563eb; --bg:#f9fafb; --card-bg:#ffffff; --border:#e5e7eb; --radius:10px; --font:system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; } body { margin:0; background:var(--bg); display:flex; align-items:center; justify-content:center; height:100vh; font-family:var(--font); } .card { width:100%; max-width:480px; background:var(--card-bg); padding:28px 32px; border:1px solid var(--border); border-radius:var(--radius); box-shadow:0 10px 30px rgba(0,0,0,0.06); text-align:center; } h1 { margin:0 0 8px; font-size:22px; } p { color:#4b5563; font-size:14px; margin:0 0 14px; } a { color:var(--primary); text-decoration:none; } </style> </head> <body> <div class="card"> <h1>Authorization successful</h1> <p>You can close this window. Redirecting you back to your application\u2026</p> <p><a href="${redirectUrl}">Continue</a></p> </div> <script>setTimeout(function(){ location.replace(${JSON.stringify(redirectUrl)}); }, 900);</script> </body> </html>`; app.get(`${basePath}/authorize`, async (req, res) => { try { const q = req.query; const first = (v) => Array.isArray(v) ? v[0] : v; const params = { response_type: first(q.response_type), client_id: first(q.client_id), redirect_uri: first(q.redirect_uri), scope: first(q.scope), state: first(q.state), resource: first(q.resource), code_challenge: first(q.code_challenge), code_challenge_method: first(q.code_challenge_method) }; const requestContext = { ipAddress: req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.ip || req.connection.remoteAddress || "0.0.0.0", userAgent: req.headers["user-agent"] }; const result = await oauthServer.handleAuthorizationRequest(params, void 0, req.session, requestContext); if ("error" in result) { return res.status(400).json(result); } if ("loginPage" in result) { return res.type("html").send(result.loginPage); } if ("redirectUrl" in result) { res.type("html").send(renderSuccessPage(result.redirectUrl)); } } catch (error) { console.error("Authorization error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); app.post(`${basePath}/authorize`, async (req, res) => { try { const b = req.body; const first = (v) => Array.isArray(v) ? v[0] : v; const params = { response_type: first(b.response_type), client_id: first(b.client_id), redirect_uri: first(b.redirect_uri), scope: first(b.scope), state: first(b.state), resource: first(b.resource), code_challenge: first(b.code_challenge), code_challenge_method: first(b.code_challenge_method) }; const credentials = first(b.username) && first(b.password) ? { username: first(b.username), password: first(b.password) } : void 0; const requestContext = { ipAddress: req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.ip || req.connection.remoteAddress || "0.0.0.0", userAgent: req.headers["user-agent"] }; const result = await oauthServer.handleAuthorizationRequest(params, credentials, req.session, requestContext); if ("error" in result) { return res.status(400).json(result); } if ("loginPage" in result) { return res.type("html").send(result.loginPage); } if ("redirectUrl" in result) { res.type("html").send(renderSuccessPage(result.redirectUrl)); } } catch (error) { console.error("Authorization error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); app.post(`${basePath}/token`, async (req, res) => { try { const params = { grant_type: req.body.grant_type, client_id: req.body.client_id, client_secret: req.body.client_secret, code: req.body.code, redirect_uri: req.body.redirect_uri, refresh_token: req.body.refresh_token, scope: req.body.scope, resource: req.body.resource, code_verifier: req.body.code_verifier }; const result = await oauthServer.handleTokenRequest(params); if ("error" in result) { return res.status(400).json(result); } res.json(result); } catch (error) { console.error("Token error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); app.post(`${basePath}/introspect`, async (req, res) => { try { const { token } = req.body; if (!token) { return res.status(400).json({ error: "invalid_request", error_description: "token parameter is required" }); } const result = await oauthServer.introspectToken(token); res.json(result); } catch (error) { console.error("Introspection error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); app.post(`${basePath}/revoke`, async (req, res) => { try { const { token, client_id } = req.body; if (!token || !client_id) { return res.status(400).json({ error: "invalid_request", error_description: "token and client_id parameters are required" }); } const result = await oauthServer.revokeToken(token, client_id); res.json(result); } catch (error) { console.error("Revocation error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); app.get(`${basePath}/userinfo`, async (req, res) => { try { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith("Bearer ")) { return res.status(401).json({ error: "invalid_token", error_description: "Bearer token required" }); } const token = authHeader.substring(7); const result = await oauthServer.getUserInfo(token); if ("error" in result) { return res.status(401).json(result); } res.json(result); } catch (error) { console.error("User info error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); app.post(`${basePath}/register`, async (req, res) => { try { const result = await oauthServer.registerClient(req.body); if ("error" in result) { return res.status(400).json(result); } res.status(201).json(result); } catch (error) { console.error("Client registration error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); app.get(`${basePath}/.well-known/oauth-authorization-server`, (req, res) => { const metadata = oauthServer.getAuthorizationServerMetadata(); res.json(metadata); }); app.get(`${basePath}/.well-known/jwks.json`, (req, res) => { res.json({ keys: [] }); }); app.get(`${basePath}/.well-known/oauth-protected-resource`, (req, res) => { const metadata = oauthServer.getProtectedResourceMetadata(); res.json(metadata); }); app.get(`${basePath}/health`, (req, res) => { res.json({ status: "ok", service: "mcp-oauth-server", version: "1.0.0", timestamp: (/* @__PURE__ */ new Date()).toISOString() }); }); app.get(`${basePath}/admin/clients`, async (req, res) => { try { const clients = await oauthServer.listClients(); res.json(clients); } catch (error) { console.error("List clients error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); app.get(`${basePath}/admin/users`, async (req, res) => { try { const users = await oauthServer.listUsers(); res.json(users); } catch (error) { console.error("List users error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); app.get(`${basePath}/admin/stats`, async (req, res) => { try { const stats = await oauthServer.getStats(); res.json(stats); } catch (error) { console.error("Stats error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); } var MCPOAuthHttpServer = class { app; oauthServer; config; constructor(oauthServer, config) { this.oauthServer = oauthServer; this.config = config; this.app = (0, import_express.default)(); this.setupMiddleware(); this.setupRoutes(); this.setupErrorHandling(); } setupMiddleware() { const httpConfig = this.config.http || {}; if (httpConfig.trustProxy !== void 0) { this.app.set("trust proxy", httpConfig.trustProxy); } if (httpConfig.enableHelmet !== false) { this.app.use((0, import_helmet.default)({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", "data:", "https:"] } } })); } if (httpConfig.enableCompression !== false) { this.app.use((0, import_compression.default)()); } if (httpConfig.enableRateLimit !== false) { const rateLimitConfig = httpConfig.rateLimitConfig || {}; const limiter = (0, import_express_rate_limit.default)({ windowMs: rateLimitConfig.windowMs || 15 * 60 * 1e3, // 15 minutes max: rateLimitConfig.max || 100, // limit each IP to 100 requests per windowMs message: rateLimitConfig.message || "Too many requests from this IP, please try again later.", standardHeaders: rateLimitConfig.standardHeaders !== false, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: rateLimitConfig.legacyHeaders !== false // Disable the `X-RateLimit-*` headers }); this.app.use(limiter); } const corsConfig = httpConfig.cors || { origin: true, // Allow all origins by default credentials: true, exposedHeaders: ["mcp-session-id"], allowedHeaders: ["Content-Type", "mcp-session-id", "accept", "last-event-id", "Authorization"], methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] }; this.app.use((0, import_cors.default)(corsConfig)); this.app.use(import_express.default.json({ limit: httpConfig.jsonLimit || "10mb" })); this.app.use(import_express.default.urlencoded({ extended: true, limit: httpConfig.urlencodedLimit || "10mb" })); } setupRoutes() { this.app.get("/authorize", async (req, res) => { try { const params = { response_type: req.query.response_type, client_id: req.query.client_id, redirect_uri: req.query.redirect_uri, scope: req.query.scope, state: req.query.state, resource: req.query.resource, code_challenge: req.query.code_challenge, code_challenge_method: req.query.code_challenge_method }; const requestContext = { ipAddress: req.ip || req.connection.remoteAddress || "0.0.0.0", userAgent: req.headers["user-agent"] }; const result = await this.oauthServer.handleAuthorizationRequest(params, void 0, req.session, requestContext); if ("error" in result) { return res.status(400).json(result); } if ("loginPage" in result) { return res.type("html").send(result.loginPage); } if ("redirectUrl" in result) { res.redirect(result.redirectUrl); } } catch (error) { console.error("Authorization error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); this.app.post("/authorize", async (req, res) => { try { const params = { response_type: req.body.response_type, client_id: req.body.client_id, redirect_uri: req.body.redirect_uri, scope: req.body.scope, state: req.body.state, resource: req.body.resource, code_challenge: req.body.code_challenge, code_challenge_method: req.body.code_challenge_method }; const credentials = req.body.username && req.body.password ? { username: req.body.username, password: req.body.password } : void 0; const requestContext = { ipAddress: req.ip || req.connection.remoteAddress || "0.0.0.0", userAgent: req.headers["user-agent"] }; const result = await this.oauthServer.handleAuthorizationRequest(params, credentials, req.session, requestContext); if ("error" in result) { return res.status(400).json(result); } if ("loginPage" in result) { return res.type("html").send(result.loginPage); } if ("redirectUrl" in result) { res.redirect(result.redirectUrl); } } catch (error) { console.error("Authorization error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); this.app.post("/token", async (req, res) => { try { const params = { grant_type: req.body.grant_type, client_id: req.body.client_id, client_secret: req.body.client_secret, code: req.body.code, redirect_uri: req.body.redirect_uri, refresh_token: req.body.refresh_token, scope: req.body.scope, resource: req.body.resource, code_verifier: req.body.code_verifier }; const result = await this.oauthServer.handleTokenRequest(params); if ("error" in result) { return res.status(400).json(result); } res.json(result); } catch (error) { console.error("Token error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); this.app.post("/introspect", async (req, res) => { try { const { token } = req.body; if (!token) { return res.status(400).json({ error: "invalid_request", error_description: "token parameter is required" }); } const result = await this.oauthServer.introspectToken(token); res.json(result); } catch (error) { console.error("Introspection error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); this.app.post("/revoke", async (req, res) => { try { const { token, client_id } = req.body; if (!token || !client_id) { return res.status(400).json({ error: "invalid_request", error_description: "token and client_id parameters are required" }); } const result = await this.oauthServer.revokeToken(token, client_id); res.json(result); } catch (error) { console.error("Revocation error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); this.app.get("/userinfo", async (req, res) => { try { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith("Bearer ")) { return res.status(401).json({ error: "invalid_token", error_description: "Bearer token required" }); } const token = authHeader.substring(7); const result = await this.oauthServer.getUserInfo(token); if ("error" in result) { return res.status(401).json(result); } res.json(result); } catch (error) { console.error("User info error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); this.app.post("/register", async (req, res) => { try { const result = await this.oauthServer.registerClient(req.body); if ("error" in result) { return res.status(400).json(result); } res.status(201).json(result); } catch (error) { console.error("Client registration error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); this.app.get("/.well-known/oauth-authorization-server", (req, res) => { const metadata = this.oauthServer.getAuthorizationServerMetadata(); res.json(metadata); }); this.app.get("/.well-known/jwks.json", (req, res) => { res.json({ keys: [] }); }); this.app.get("/.well-known/oauth-protected-resource", (req, res) => { const metadata = this.oauthServer.getProtectedResourceMetadata(); res.json(metadata); }); this.app.get("/health", (req, res) => { res.json({ status: "ok", service: "mcp-oauth-server", version: "1.0.0", timestamp: (/* @__PURE__ */ new Date()).toISOString() }); }); this.app.get("/admin/clients", async (req, res) => { try { const clients = await this.oauthServer.listClients(); res.json(clients); } catch (error) { console.error("List clients error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); this.app.get("/admin/users", async (req, res) => { try { const users = await this.oauthServer.listUsers(); res.json(users); } catch (error) { console.error("List users error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); this.app.get("/admin/stats", async (req, res) => { try { const stats = await this.oauthServer.getStats(); res.json(stats); } catch (error) { console.error("Stats error:", error); res.status(500).json({ error: "server_error", error_description: "Internal server error" }); } }); } setupErrorHandling() { this.app.use((error, req, res, next) => { console.error("Unhandled error:", error); const isDevelopment = process.env.NODE_ENV === "development"; res.status(500).json({ error: "server_error", error_description: isDevelopment ? error.message : "Internal server error", ...isDevelopment && { stack: error.stack } }); }); } getApp() { return this.app; } async start(port) { return new Promise((resolve, reject) => { const server = this.app.listen(port, () => { console.log(`MCP OAuth Server running on port ${port}`); console.log(`Health check: http://localhost:${port}/health`); console.log(`Authorization endpoint: http://localhost:${port}/authorize`); console.log(`Token endpoint: http://localhost:${port}/token`); console.log(`Discovery: http://localhost:${port}/.well-known/oauth-authorization-server`); resolve(); }); server.on("error", (error) => { console.error("Failed to start server:", error); reject(error); }); }); } }; // src/storage/memory-storage.ts var MemoryStorage = class { clients = /* @__PURE__ */ new Map(); users = /* @__PURE__ */ new Map(); authorizationCodes = /* @__PURE__ */ new Map(); accessTokens = /* @__PURE__ */ new Map(); refreshTokens = /* @__PURE__ */ new Map(); // ===== CLIENT MANAGEMENT ===== async createClient(client) { this.clients.set(client.id, client); } async getClient(clientId) { return this.clients.get(clientId) || null; } async listClients() { return Array.from(this.clients.values()); } async updateClient(clientId, updates) { const client = this.clients.get(clientId); if (client) { this.clients.set(clientId, { ...client, ...updates, updatedAt: /* @__PURE__ */ new Date() }); } } async deleteClient(clientId) { this.clients.delete(clientId); } // ===== USER MANAGEMENT ===== async createUser(user) { this.users.set(user.id, user); } async getUser(userId) { return this.users.get(userId) || null; } async getUserByUsername(username) { for (const user of this.users.values()) { if (user.username === username) { return user; } } return null; } async listUsers() { return Array.from(this.users.values()); } async updateUser(userId, updates) { const user = this.users.get(userId); if (user) { this.users.set(userId, { ...user, ...updates, updatedAt: /* @__PURE__ */ new Date() }); } } async deleteUser(userId) { this.users.delete(userId); } // ===== AUTHORIZATION CODES ===== async createAuthorizationCode(code) { this.authorizationCodes.set(code.code, code); } async getAuthorizationCode(code) { return this.authorizationCodes.get(code) || null; } async deleteAuthorizationCode(code) { this.authorizationCodes.delete(code); } async cleanupExpiredCodes() { const now = /* @__PURE__ */ new Date(); for (const [code, authCode] of this.authorizationCodes.entries()) { if (authCode.expiresAt < now) { this.authorizationCodes.delete(code); } } } // ===== ACCESS TOKENS ===== async createAccessToken(token) { this.accessTokens.set(token.token, token); } async getAccessToken(token) { return this.accessTokens.get(token) || null; } async deleteAccessToken(token) { this.accessTokens.delete(token); } async cleanupExpiredTokens() { const now = /* @__PURE__ */ new Date(); for (const [token, accessToken] of this.accessTokens.entries()) { if (accessToken.expiresAt < now) { this.accessTokens.delete(token); } } } // ===== REFRESH TOKENS ===== async createRefreshToken(token) { this.refreshTokens.set(token.token, token); } async getRefreshToken(token) { return this.refreshTokens.get(token) || null; } async deleteRefreshToken(token) { this.refreshTokens.delete(token); } async deleteRefreshTokensByAccessToken(accessTokenId) { for (const [token, refreshToken] of this.refreshTokens.entries()) { if (refreshToken.accessTokenId === accessTokenId) { this.refreshTokens.delete(token); } } }