Skip to main content
Glama
oauth.ts6.2 kB
import { Config } from './types.js'; import { randomBytes, timingSafeEqual as cryptoTimingSafeEqual, createHash } from 'crypto'; // Simple in-memory token store const activeTokens = new Map<string, { expiresAt: number }>(); // Authorization code store (code -> { codeChallenge, clientId, redirectUri, expiresAt }) const authorizationCodes = new Map<string, { codeChallenge: string; codeChallengeMethod: string; clientId: string; redirectUri: string; expiresAt: number; }>(); // Token expiration time in seconds (1 hour) const TOKEN_EXPIRATION = 3600; // Auth code expiration (10 minutes) const AUTH_CODE_EXPIRATION = 600; // Clean up expired tokens and codes periodically setInterval(() => { const now = Date.now(); for (const [token, data] of activeTokens.entries()) { if (data.expiresAt < now) { activeTokens.delete(token); } } for (const [code, data] of authorizationCodes.entries()) { if (data.expiresAt < now) { authorizationCodes.delete(code); } } }, 60000); export interface TokenResponse { access_token: string; token_type: string; expires_in: number; } export interface TokenError { error: string; error_description: string; } function timingSafeEqual(a: string, b: string): boolean { const bufA = Buffer.from(a); const bufB = Buffer.from(b); if (bufA.length !== bufB.length) { return false; } return cryptoTimingSafeEqual(bufA, bufB); } export function validateClientCredentials( clientId: string, clientSecret: string, config: Config ): boolean { if (!config.oauthClientId || !config.oauthClientSecret) { return false; } // Use constant-time comparison to prevent timing attacks try { const idMatch = timingSafeEqual(clientId, config.oauthClientId); const secretMatch = timingSafeEqual(clientSecret, config.oauthClientSecret); return idMatch && secretMatch; } catch { return false; } } export function validateClientId(clientId: string, config: Config): boolean { if (!config.oauthClientId) { return false; } try { return timingSafeEqual(clientId, config.oauthClientId); } catch { return false; } } export function issueToken(): TokenResponse { const token = randomBytes(32).toString('hex'); const expiresAt = Date.now() + (TOKEN_EXPIRATION * 1000); activeTokens.set(token, { expiresAt }); console.error(`[OAuth] Issued new token, expires in ${TOKEN_EXPIRATION}s`); return { access_token: token, token_type: 'Bearer', expires_in: TOKEN_EXPIRATION, }; } export function validateAccessToken(token: string, config: Config): boolean { // First check if it's a static API key (backward compatibility) if (config.apiKey && token === config.apiKey) { return true; } // Then check OAuth tokens const tokenData = activeTokens.get(token); if (!tokenData) { return false; } if (tokenData.expiresAt < Date.now()) { activeTokens.delete(token); return false; } return true; } // Authorization Code flow functions export function createAuthorizationCode( clientId: string, redirectUri: string, codeChallenge: string, codeChallengeMethod: string ): string { const code = randomBytes(32).toString('hex'); const expiresAt = Date.now() + (AUTH_CODE_EXPIRATION * 1000); authorizationCodes.set(code, { codeChallenge, codeChallengeMethod, clientId, redirectUri, expiresAt, }); console.error(`[OAuth] Created authorization code for client: ${clientId}`); return code; } export function validateAuthorizationCode( code: string, clientId: string, redirectUri: string, codeVerifier: string ): { valid: boolean; error?: string } { const codeData = authorizationCodes.get(code); if (!codeData) { return { valid: false, error: 'Invalid authorization code' }; } // Code can only be used once authorizationCodes.delete(code); if (codeData.expiresAt < Date.now()) { return { valid: false, error: 'Authorization code expired' }; } if (codeData.clientId !== clientId) { return { valid: false, error: 'Client ID mismatch' }; } if (codeData.redirectUri !== redirectUri) { return { valid: false, error: 'Redirect URI mismatch' }; } // Validate PKCE if (!validatePKCE(codeVerifier, codeData.codeChallenge, codeData.codeChallengeMethod)) { return { valid: false, error: 'Invalid code verifier' }; } return { valid: true }; } function validatePKCE( codeVerifier: string, codeChallenge: string, codeChallengeMethod: string ): boolean { if (codeChallengeMethod === 'S256') { // SHA256 hash of verifier, base64url encoded const hash = createHash('sha256') .update(codeVerifier) .digest('base64url'); return hash === codeChallenge; } else if (codeChallengeMethod === 'plain') { return codeVerifier === codeChallenge; } return false; } export function parseBasicAuth(authHeader: string): { clientId: string; clientSecret: string } | null { if (!authHeader.startsWith('Basic ')) { return null; } try { const base64 = authHeader.substring(6); const decoded = Buffer.from(base64, 'base64').toString('utf-8'); const colonIndex = decoded.indexOf(':'); if (colonIndex === -1) { return null; } const clientId = decoded.substring(0, colonIndex); const clientSecret = decoded.substring(colonIndex + 1); if (!clientId || !clientSecret) { return null; } return { clientId, clientSecret }; } catch { return null; } } export async function parseRequestBody(req: import('http').IncomingMessage): Promise<string> { return new Promise((resolve, reject) => { let body = ''; req.on('data', (chunk) => { body += chunk.toString(); }); req.on('end', () => { resolve(body); }); req.on('error', reject); }); } export function parseFormUrlEncoded(body: string): Record<string, string> { const params: Record<string, string> = {}; for (const pair of body.split('&')) { const [key, value] = pair.split('='); if (key && value !== undefined) { params[decodeURIComponent(key)] = decodeURIComponent(value.replace(/\+/g, ' ')); } } return params; }

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/bshandley/homelab-mcp'

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