Skip to main content
Glama
AuthorizationServerDiscovery.ts7.72 kB
/** * db-mcp - Authorization Server Discovery (RFC 8414) * * Discovers and caches OAuth 2.0 Authorization Server Metadata * as specified in RFC 8414. * * @see https://datatracker.ietf.org/doc/html/rfc8414 */ import type { AuthorizationServerMetadata, AuthServerDiscoveryConfig } from './types.js'; import { AuthServerDiscoveryError } from './errors.js'; import { createModuleLogger, ERROR_CODES } from '../utils/logger.js'; const logger = createModuleLogger('AUTH'); // ============================================================================= // Authorization Server Discovery // ============================================================================= /** * Authorization Server Metadata Discovery * * Fetches and caches OAuth 2.0 authorization server metadata * from the /.well-known/oauth-authorization-server endpoint. */ export class AuthorizationServerDiscovery { private readonly authServerUrl: string; private readonly cacheTtl: number; private readonly timeout: number; private cachedMetadata: AuthorizationServerMetadata | null = null; private cacheExpiry = 0; constructor(config: AuthServerDiscoveryConfig) { // Normalize URL (remove trailing slash) this.authServerUrl = config.authServerUrl.replace(/\/+$/, ''); this.cacheTtl = config.cacheTtl ?? 3600; this.timeout = config.timeout ?? 5000; logger.info('INIT', `Authorization Server Discovery initialized for: ${this.authServerUrl}`); } /** * Discover authorization server metadata * * Fetches from /.well-known/oauth-authorization-server * Results are cached for cacheTtl seconds. * * @returns Authorization server metadata * @throws AuthServerDiscoveryError if discovery fails */ async discover(): Promise<AuthorizationServerMetadata> { // Check cache if (this.cachedMetadata && Date.now() < this.cacheExpiry) { logger.info('CACHE_HIT', 'Using cached authorization server metadata'); return this.cachedMetadata; } const metadataUrl = `${this.authServerUrl}/.well-known/oauth-authorization-server`; logger.info('DISCOVERY', `Fetching authorization server metadata from: ${metadataUrl}`); try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); const response = await fetch(metadataUrl, { method: 'GET', headers: { 'Accept': 'application/json' }, signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${String(response.status)}: ${response.statusText}`); } const metadata = await response.json() as AuthorizationServerMetadata; // Validate required fields per RFC 8414 this.validateMetadata(metadata); // Cache the metadata this.cachedMetadata = metadata; this.cacheExpiry = Date.now() + (this.cacheTtl * 1000); logger.info('DISCOVERY_SUCCESS', `Authorization server metadata cached for ${String(this.cacheTtl)}s`); return metadata; } catch (error) { const cause = error instanceof Error ? error : new Error(String(error)); logger.error( ERROR_CODES.AUTH.DISCOVERY_FAILED, `Failed to discover authorization server: ${this.authServerUrl}`, { operation: 'discover', entityId: this.authServerUrl, error: cause } ); throw new AuthServerDiscoveryError(this.authServerUrl, cause); } } /** * Validate required metadata fields per RFC 8414 */ private validateMetadata(metadata: AuthorizationServerMetadata): void { if (!metadata.issuer) { throw new Error('Missing required field: issuer'); } if (!metadata.token_endpoint) { throw new Error('Missing required field: token_endpoint'); } // Validate issuer matches the expected URL // Per RFC 8414, issuer MUST be identical to the authorization server URL const expectedIssuer = this.authServerUrl; if (metadata.issuer !== expectedIssuer) { logger.warning( 'ISSUER_MISMATCH', `Issuer mismatch: expected ${expectedIssuer}, got ${metadata.issuer}` ); // Note: This is a warning, not an error, as some auth servers may use different URLs } } /** * Get cached metadata (throws if not discovered) */ getMetadata(): AuthorizationServerMetadata { if (!this.cachedMetadata) { throw new Error('Authorization server metadata not yet discovered. Call discover() first.'); } return this.cachedMetadata; } /** * Get JWKS URI from metadata * * @throws Error if metadata not discovered or jwks_uri not present */ getJwksUri(): string { const metadata = this.getMetadata(); if (!metadata.jwks_uri) { throw new Error('Authorization server does not provide jwks_uri'); } return metadata.jwks_uri; } /** * Get token endpoint from metadata */ getTokenEndpoint(): string { return this.getMetadata().token_endpoint; } /** * Get issuer from metadata */ getIssuer(): string { return this.getMetadata().issuer; } /** * Get registration endpoint from metadata (RFC 7591) * * @returns Registration endpoint or null if not supported */ getRegistrationEndpoint(): string | null { return this.getMetadata().registration_endpoint ?? null; } /** * Check if dynamic client registration is supported */ supportsClientRegistration(): boolean { return this.getRegistrationEndpoint() !== null; } /** * Get supported scopes from metadata */ getSupportedScopes(): string[] { return this.getMetadata().scopes_supported ?? []; } /** * Check if a specific scope is supported */ isScopeSupported(scope: string): boolean { const supportedScopes = this.getSupportedScopes(); // If no scopes are listed, assume all scopes are supported return supportedScopes.length === 0 || supportedScopes.includes(scope); } /** * Clear cached metadata */ clearCache(): void { this.cachedMetadata = null; this.cacheExpiry = 0; logger.info('CACHE_CLEARED', 'Authorization server metadata cache cleared'); } /** * Check if cache is valid */ isCacheValid(): boolean { return this.cachedMetadata !== null && Date.now() < this.cacheExpiry; } /** * Get the authorization server URL */ getAuthServerUrl(): string { return this.authServerUrl; } } // ============================================================================= // Factory Function // ============================================================================= /** * Create an Authorization Server Discovery instance */ export function createAuthServerDiscovery( authServerUrl: string, options?: Partial<Omit<AuthServerDiscoveryConfig, 'authServerUrl'>> ): AuthorizationServerDiscovery { return new AuthorizationServerDiscovery({ authServerUrl, cacheTtl: options?.cacheTtl, timeout: options?.timeout }); }

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/neverinfamous/db-mcp'

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