Skip to main content
Glama
oauth-callback-server.ts12.1 kB
/** * OAuth callback server for WordPress implicit flow */ import express from 'express'; import { Server } from 'node:http'; import { EventEmitter } from 'node:events'; import { OAuthCallbackServerOptions, WPTokens, OAuthError } from './oauth-types.js'; import { writeTokens } from './persistent-auth-config.js'; import { logger } from './utils.js'; /** * HTML page for handling OAuth 2.1 authorization code callback * Updated for MCP Authorization specification 2025-06-18 compliance */ const AUTHORIZATION_CODE_HTML = ` <!DOCTYPE html> <html> <head> <title>MCP Client Authorization</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 40px; background: #f5f5f5; min-height: 100vh; display: flex; align-items: center; justify-content: center; } .container { background: white; padding: 40px; border-radius: 12px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); text-align: center; max-width: 500px; width: 100%; } h1 { color: #333; margin-bottom: 20px; } .success { color: #27ae60; font-size: 18px; margin: 20px 0; } .error { color: #e74c3c; font-size: 18px; margin: 20px 0; } .loading { color: #3498db; font-size: 18px; margin: 20px 0; } .details { color: #666; margin-top: 10px; font-size: 14px; } .spinner { border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 20px auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> </head> <body> <div class="container"> <h1>MCP Client Authorization</h1> <div class="spinner" id="spinner"></div> <div id="status" class="loading">Processing authorization code...</div> <div id="details" class="details"></div> </div> <script> function displayStatus(message, type = 'loading') { const statusEl = document.getElementById('status'); const spinnerEl = document.getElementById('spinner'); statusEl.textContent = message; statusEl.className = type; if (type !== 'loading') { spinnerEl.style.display = 'none'; } } function displayDetails(message) { document.getElementById('details').textContent = message; } // Handle both OAuth 2.1 authorization code flow and implicit flow const urlParams = new URLSearchParams(window.location.search); const hashParams = new URLSearchParams(window.location.hash.substring(1)); // Check for authorization code first (OAuth 2.1 flow) if (urlParams.has('code')) { const authData = { code: urlParams.get('code'), state: urlParams.get('state'), // Additional parameters for validation iss: urlParams.get('iss'), }; // Send authorization code to server for token exchange fetch('/oauth/callback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(authData) }) .then(response => { if (response.ok) { displayStatus('✅ Authorization successful!', 'success'); displayDetails('You can safely close this window.'); } else { return response.json().then(err => { throw new Error(err.error || 'Failed to exchange authorization code'); }); } }) .catch(error => { displayStatus('❌ Error completing authorization', 'error'); displayDetails(error.message + '. Please try again.'); }); } // Check for access token in URL fragment (implicit flow) else if (hashParams.has('access_token')) { const tokens = { access_token: hashParams.get('access_token'), token_type: hashParams.get('token_type') || 'Bearer', expires_in: hashParams.get('expires_in') ? parseInt(hashParams.get('expires_in')) : 3600, scope: hashParams.get('scope'), state: hashParams.get('state'), obtained_at: Date.now() }; // Send tokens to server for storage fetch('/oauth/tokens', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(tokens) }) .then(response => { if (response.ok) { displayStatus('✅ Authorization successful!', 'success'); displayDetails('You can safely close this window.'); } else { return response.json().then(err => { throw new Error(err.error || 'Failed to store access token'); }); } }) .catch(error => { displayStatus('❌ Error completing authorization', 'error'); displayDetails(error.message + '. Please try again.'); }); } // Check for errors in query parameters else if (urlParams.has('error')) { const error = urlParams.get('error'); const errorDescription = urlParams.get('error_description'); displayStatus('❌ Authorization failed', 'error'); displayDetails(\`Error: \${error}\${errorDescription ? ' - ' + errorDescription : ''}\`); } // Check for errors in URL fragment (implicit flow errors) else if (hashParams.has('error')) { const error = hashParams.get('error'); const errorDescription = hashParams.get('error_description'); displayStatus('❌ Authorization failed', 'error'); displayDetails(\`Error: \${error}\${errorDescription ? ' - ' + errorDescription : ''}\`); } // No authorization code or access token found else { displayStatus('❌ No authorization code or access token received', 'error'); displayDetails('Please try the authorization process again.'); } </script> </body> </html> `; export class OAuthCallbackServer { private app: express.Application; private server: Server | null = null; private events: EventEmitter; private options: OAuthCallbackServerOptions; constructor(options: OAuthCallbackServerOptions, events: EventEmitter) { this.options = options; this.events = events; this.app = express(); this.setupRoutes(); } private setupRoutes(): void { // Parse JSON bodies this.app.use(express.json()); // Serve the authorization code callback page this.app.get('/oauth/callback', (req, res) => { logger.oauth('OAuth 2.1 authorization callback page requested'); res.send(AUTHORIZATION_CODE_HTML); }); // Handle authorization code from OAuth 2.1 flow this.app.post('/oauth/callback', async (req, res) => { try { const { code, state, iss } = req.body; if (!code) { throw new OAuthError('No authorization code received'); } logger.oauth('OAuth 2.1 authorization code received'); logger.debug('Authorization code details', 'OAUTH', { codeLength: code.length, state: state || 'none', issuer: iss || 'none', }); // Emit authorization code event for processing this.events.emit('oauth-code-received', { code, state, iss }); res.json({ success: true, message: 'Authorization code received successfully' }); } catch (error) { logger.error('Error processing authorization code', 'OAUTH', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.events.emit('oauth-error', new OAuthError(errorMessage)); res.status(400).json({ error: errorMessage }); } }); // Legacy endpoint for backward compatibility (tokens endpoint for implicit flow) this.app.post('/oauth/tokens', async (req, res) => { try { const tokens = req.body as WPTokens; if (!tokens.access_token) { throw new OAuthError('No access token received'); } logger.oauth('Legacy OAuth tokens received (implicit flow)'); logger.debug('Token details', 'OAUTH', { tokenType: tokens.token_type, expiresIn: tokens.expires_in, scope: tokens.scope, tokenLength: tokens.access_token.length, }); // Store the tokens await writeTokens(this.options.serverUrlHash, tokens); // Emit success event this.events.emit('oauth-success', tokens); res.json({ success: true, message: 'Tokens saved successfully' }); } catch (error) { logger.error('Error processing OAuth tokens', 'OAUTH', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.events.emit('oauth-error', new OAuthError(errorMessage)); res.status(400).json({ error: errorMessage }); } }); // Health check endpoint this.app.get('/oauth/health', (req, res) => { res.json({ status: 'ok', serverHash: this.options.serverUrlHash, timestamp: new Date().toISOString(), }); }); // Error handling middleware this.app.use( (error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { logger.error('Express server error', 'OAUTH', error); res.status(500).json({ error: 'Internal server error' }); } ); } async start(): Promise<void> { return new Promise((resolve, reject) => { try { this.server = this.app.listen(this.options.port, this.options.host, () => { logger.oauth( `OAuth callback server listening on http://${this.options.host}:${this.options.port}` ); resolve(); }); this.server.on('error', (error: Error) => { logger.error('OAuth callback server error', 'OAUTH', error); reject(error); }); // Set timeout for server startup if (this.options.timeout) { setTimeout(() => { if (!this.server?.listening) { reject(new OAuthError('OAuth callback server startup timeout', 'TIMEOUT')); } }, this.options.timeout); } } catch (error) { reject(error); } }); } async stop(): Promise<void> { return new Promise(resolve => { if (this.server) { this.server.close(() => { logger.oauth('OAuth callback server stopped'); this.server = null; resolve(); }); } else { resolve(); } }); } getCallbackUrl(): string { return `http://${this.options.host}:${this.options.port}/oauth/callback`; } isRunning(): boolean { return this.server?.listening ?? false; } } /** * Setup WordPress OAuth callback server */ export function setupWPOAuthCallbackServer( options: OAuthCallbackServerOptions, events: EventEmitter ): OAuthCallbackServer { return new OAuthCallbackServer(options, events); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Automattic/mcp-wordpress-remote'

If you have feedback or need assistance with the MCP directory API, please join our Discord server