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
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, {
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);
}
}
}