UNPKG

automagik-genie

Version:

Self-evolving AI agent orchestration framework with Model Context Protocol support

208 lines (207 loc) 8.21 kB
"use strict"; /** * OAuth2.1 Endpoints for Genie MCP Server * * Implements: * - /.well-known/oauth-protected-resource (RFC 9728) * - /oauth/token (client_credentials + authorization_code flows) * * Note: We implement our own token endpoint because the MCP SDK doesn't support * client_credentials grant type yet (it only supports authorization_code and refresh_token). */ Object.defineProperty(exports, "__esModule", { value: true }); exports.generateResourceMetadata = generateResourceMetadata; exports.handleProtectedResourceMetadata = handleProtectedResourceMetadata; exports.handleTokenEndpoint = handleTokenEndpoint; const oauth_session_manager_js_1 = require("./oauth-session-manager.js"); // Dynamic import of OAuth2 utilities let generateAccessTokenFn; let validateClientCredentialsFn; function loadOAuth2Utils() { if (!generateAccessTokenFn) { const oauth2Utils = require('../../cli/lib/oauth2-utils.js'); generateAccessTokenFn = oauth2Utils.generateAccessToken; validateClientCredentialsFn = oauth2Utils.validateClientCredentials; } return { generateAccessTokenFn, validateClientCredentialsFn }; } /** * Generate protected resource metadata * * @param serverUrl - Base URL of the MCP server (e.g., 'http://localhost:{MCP_PORT}') * @returns RFC 9728 compliant metadata */ function generateResourceMetadata(serverUrl) { return { resource: `${serverUrl}/mcp`, authorization_servers: [`${serverUrl}`], // Self-issued tokens bearer_methods_supported: ['header'], // Authorization: Bearer <token> scopes_supported: ['mcp:read', 'mcp:write'], resource_signing_alg_values_supported: ['RS256'], }; } /** * Handle /.well-known/oauth-protected-resource endpoint * Returns metadata as JSON per RFC 9728 */ function handleProtectedResourceMetadata(req, res, serverUrl) { const metadata = generateResourceMetadata(serverUrl); res.status(200) .set('Cache-Control', 'public, max-age=3600') // Cache for 1 hour .json(metadata); } /** * Parse Basic authentication header * Returns { username, password } or null if invalid */ function parseBasicAuth(authHeader) { if (!authHeader || !authHeader.startsWith('Basic ')) { return null; } try { const base64Credentials = authHeader.slice(6); // Remove 'Basic ' const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8'); const [username, password] = credentials.split(':', 2); if (!username || !password) { return null; } return { username, password }; } catch { return null; } } // parseFormBody removed - Express handles body parsing via express.json() and express.urlencoded() /** * Handle /oauth/token endpoint (client_credentials + authorization_code flows) * Implements RFC 6749 Section 4.1 (authorization_code) and 4.4 (client_credentials) * * Express middleware - body is already parsed by express.json() / express.urlencoded() */ async function handleTokenEndpoint(req, res, oauth2Config, serverUrl) { const { generateAccessTokenFn, validateClientCredentialsFn } = loadOAuth2Utils(); const grantType = req.body.grant_type; // Route to appropriate grant handler if (grantType === 'authorization_code') { return handleAuthorizationCodeGrant(req, res, oauth2Config, serverUrl, generateAccessTokenFn); } else if (grantType === 'client_credentials') { return handleClientCredentialsGrant(req, res, oauth2Config, serverUrl, generateAccessTokenFn, validateClientCredentialsFn); } else { res.status(400).json({ error: 'unsupported_grant_type', error_description: 'Supported grant types: authorization_code, client_credentials', }); return; } } /** * Handle authorization_code grant (RFC 6749 Section 4.1.3) * Validates authorization code with PKCE and issues access token */ async function handleAuthorizationCodeGrant(req, res, oauth2Config, serverUrl, generateAccessTokenFn) { const { code, redirect_uri, client_id, code_verifier } = req.body; // Validate required parameters if (!code || !redirect_uri || !client_id || !code_verifier) { res.status(400).json({ error: 'invalid_request', error_description: 'Missing required parameters: code, redirect_uri, client_id, code_verifier', }); return; } // Validate and consume authorization code (includes PKCE validation) const authCode = oauth_session_manager_js_1.oauthSessionManager.validateAndConsumeCode(code, client_id, redirect_uri, code_verifier); if (!authCode) { res.status(400).json({ error: 'invalid_grant', error_description: 'Invalid or expired authorization code, or PKCE validation failed', }); return; } try { // Generate JWT access token const accessToken = await generateAccessTokenFn(oauth2Config.signingKey, oauth2Config.issuer, `${serverUrl}/mcp`, // Audience = resource identifier client_id, oauth2Config.tokenExpiry); const tokenResponse = { access_token: accessToken, token_type: 'Bearer', expires_in: oauth2Config.tokenExpiry, scope: authCode.scope, }; res.status(200) .set('Cache-Control', 'no-store') .set('Pragma', 'no-cache') .json(tokenResponse); } catch (error) { console.error('Token generation error:', error); res.status(500).json({ error: 'server_error', error_description: 'Failed to generate access token', }); } } /** * Handle client_credentials grant (RFC 6749 Section 4.4) * Validates client credentials and issues access token */ async function handleClientCredentialsGrant(req, res, oauth2Config, serverUrl, generateAccessTokenFn, validateClientCredentialsFn) { // Parse client credentials from Authorization header OR request body let clientId = null; let clientSecret = null; // Try Authorization header first (RFC 6749 Section 2.3.1) const basicAuth = parseBasicAuth(req.headers.authorization); if (basicAuth) { clientId = basicAuth.username; clientSecret = basicAuth.password; } else { // Fall back to request body (RFC 6749 Section 2.3.1 - not recommended but allowed) clientId = req.body.client_id || null; clientSecret = req.body.client_secret || null; } // Validate client credentials presence if (!clientId || !clientSecret) { res.status(401) .set('WWW-Authenticate', 'Basic realm="Genie MCP Server"') .json({ error: 'invalid_client', error_description: 'Client authentication failed: missing credentials', }); return; } // Verify client credentials against config const validCredentials = validateClientCredentialsFn(clientId, clientSecret, oauth2Config.clientId, oauth2Config.clientSecret); if (!validCredentials) { res.status(401) .set('WWW-Authenticate', 'Basic realm="Genie MCP Server"') .json({ error: 'invalid_client', error_description: 'Client authentication failed: invalid credentials', }); return; } try { // Generate JWT access token const accessToken = await generateAccessTokenFn(oauth2Config.signingKey, oauth2Config.issuer, `${serverUrl}/mcp`, // Audience = resource identifier clientId, oauth2Config.tokenExpiry); const tokenResponse = { access_token: accessToken, token_type: 'Bearer', expires_in: oauth2Config.tokenExpiry, scope: 'mcp:read mcp:write', }; res.status(200) .set('Cache-Control', 'no-store') .set('Pragma', 'no-cache') .json(tokenResponse); } catch (error) { console.error('Token generation error:', error); res.status(500).json({ error: 'server_error', error_description: 'Failed to generate access token', }); } }