Skip to main content
Glama
oauth-handler.ts7.75 kB
import { Env, AuthSession } from './types'; import { AuthManager, AuthError } from './auth'; export class OAuthHandler { addSecurityHeaders(response: Response): Response { const headers = new Headers(response.headers); // Security headers headers.set('X-Content-Type-Options', 'nosniff'); headers.set('X-Frame-Options', 'DENY'); headers.set('X-XSS-Protection', '1; mode=block'); headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); headers.set('Content-Security-Policy', "default-src 'none'; script-src 'none'; style-src 'none';"); // CORS headers headers.set('Access-Control-Allow-Origin', '*'); headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); headers.set('Access-Control-Allow-Headers', 'Content-Type, Last-Event-ID, Mcp-Session-Id, Authorization'); headers.set('Access-Control-Max-Age', '86400'); return new Response(response.body, { status: response.status, statusText: response.statusText, headers }); } async handleAuthorize(_request: Request, env: Env): Promise<Response> { try { const authManager = new AuthManager(env); // Generate state for CSRF protection const state = authManager.generateState(); const authUrl = authManager.generateAuthURL(state); // In production, you'd store the state in a secure way (KV, session, etc.) // For now, we'll include it in the redirect return Response.redirect(authUrl, 302); } catch (error) { return new Response( error instanceof AuthError ? error.message : 'OAuth configuration error', { status: error instanceof AuthError ? error.status : 500 } ); } } async handleToken(request: Request, env: Env): Promise<Response> { if (request.method !== 'POST') { return new Response('Method not allowed', { status: 405 }); } try { const authManager = new AuthManager(env); const body = await request.formData(); const grantType = body.get('grant_type'); const code = body.get('code'); if (grantType !== 'authorization_code' || !code) { return new Response(JSON.stringify({ error: 'invalid_grant', error_description: 'Grant type must be authorization_code and code is required' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // Exchange code for GitHub access token const githubAccessToken = await authManager.exchangeCodeForToken(code.toString()); // Get user info from GitHub const user = await authManager.getUserInfo(githubAccessToken); // Create session (expires in 90 days) const expiresAt = Math.floor(Date.now() / 1000) + (90 * 24 * 60 * 60); const session: AuthSession = { userId: user.id, userLogin: user.login, expiresAt }; // Generate JWT token const jwtToken = await authManager.generateJWT(session); return this.addSecurityHeaders(new Response(JSON.stringify({ access_token: jwtToken, token_type: 'Bearer', expires_in: 90 * 24 * 60 * 60, user: { login: user.login, name: user.name } }), { headers: { 'Content-Type': 'application/json' } })); } catch (error) { const errorMessage = error instanceof AuthError ? error.message : 'Token exchange failed'; const statusCode = error instanceof AuthError ? error.status : 400; return this.addSecurityHeaders(new Response(JSON.stringify({ error: 'invalid_request', error_description: errorMessage }), { status: statusCode, headers: { 'Content-Type': 'application/json' } })); } } async handleRegister(request: Request, _env: Env): Promise<Response> { if (request.method !== 'POST') { return new Response('Method not allowed', { status: 405 }); } try { const body = await request.json() as { client_name?: string; redirect_uris?: string[] }; const { client_name, redirect_uris } = body; if (!client_name || !redirect_uris) { return new Response('Missing required fields', { status: 400 }); } // For demo purposes, generate a dummy client const clientId = 'wmata_client_' + Date.now(); const clientSecret = 'wmata_secret_' + Math.random().toString(36); return new Response(JSON.stringify({ client_id: clientId, client_secret: clientSecret, client_name, redirect_uris }), { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' } }); } catch (error) { return new Response('Invalid request', { status: 400 }); } } async handleCallback(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); const code = url.searchParams.get('code'); const error = url.searchParams.get('error'); // Note: state parameter available for CSRF protection if needed if (error) { return this.addSecurityHeaders(new Response(` <html> <head><title>Metro MCP - OAuth Error</title></head> <body> <h1>Authorization Failed</h1> <p>Error: ${error}</p> <p>Please try again or contact support.</p> </body> </html> `, { status: 400, headers: { 'Content-Type': 'text/html' } })); } if (!code) { return this.addSecurityHeaders(new Response(` <html> <head><title>Metro MCP - OAuth Error</title></head> <body> <h1>Authorization Failed</h1> <p>No authorization code received.</p> </body> </html> `, { status: 400, headers: { 'Content-Type': 'text/html' } })); } try { const authManager = new AuthManager(env); // Exchange code for token (this creates a complete authentication flow) const githubAccessToken = await authManager.exchangeCodeForToken(code); const user = await authManager.getUserInfo(githubAccessToken); // Create session const expiresAt = Math.floor(Date.now() / 1000) + (90 * 24 * 60 * 60); const session: AuthSession = { userId: user.id, userLogin: user.login, expiresAt }; const jwtToken = await authManager.generateJWT(session); return this.addSecurityHeaders(new Response(` <html> <head><title>Metro MCP - OAuth Success</title></head> <body> <h1>Authorization Successful!</h1> <p>Welcome, <strong>${user.name || user.login}</strong>!</p> <p>Your access token:</p> <textarea rows="4" cols="80" readonly>${jwtToken}</textarea> <p>Copy this token and use it in your MCP client with the Authorization header:</p> <code>Authorization: Bearer ${jwtToken}</code> <p>Token expires in 90 days.</p> </body> </html> `, { headers: { 'Content-Type': 'text/html' } })); } catch (error) { return this.addSecurityHeaders(new Response(` <html> <head><title>Metro MCP - OAuth Error</title></head> <body> <h1>Authorization Failed</h1> <p>Error processing authorization: ${error instanceof Error ? error.message : 'Unknown error'}</p> </body> </html> `, { status: 500, headers: { 'Content-Type': 'text/html' } })); } } }

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/nathanielnoyd/metro-mcp'

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