Skip to main content
Glama
iceener

Spotify Streamable MCP Server

by iceener
auth.ts6.66 kB
// Auth header middleware for Hono // Supports multiple auth strategies: OAuth, API Key, Bearer, Custom Headers import type { HttpBindings } from '@hono/node-server'; import type { MiddlewareHandler } from 'hono'; import { config } from '../../config/env.js'; import type { AuthStrategyType } from '../../shared/auth/strategy.js'; import type { ProviderTokens } from '../../shared/storage/interface.js'; import { getTokenStore } from '../../shared/storage/singleton.js'; import { logger } from '../../utils/logger.js'; /** * Auth context attached to Hono context. */ export interface AuthContext { /** Auth strategy in use */ strategy: AuthStrategyType; /** Raw authorization headers from request */ authHeaders: Record<string, string>; /** Resolved headers for API calls (includes static config headers) */ resolvedHeaders: Record<string, string>; /** Provider access token (OAuth: mapped from RS token, Bearer: from config) */ providerToken?: string; /** Full provider token info (OAuth only) */ provider?: ProviderTokens; /** Original RS token (OAuth only) */ rsToken?: string; } /** * Parse custom headers from config string. * Format: "X-Header-1:value1,X-Header-2:value2" */ function parseCustomHeaders(value: string | undefined): Record<string, string> { if (!value) return {}; const headers: Record<string, string> = {}; const pairs = value.split(','); for (const pair of pairs) { const colonIndex = pair.indexOf(':'); if (colonIndex === -1) continue; const key = pair.slice(0, colonIndex).trim(); const val = pair.slice(colonIndex + 1).trim(); if (key && val) { headers[key.toLowerCase()] = val; } } return headers; } /** * Build static headers from non-OAuth auth config. */ function buildStaticAuthHeaders(): Record<string, string> { const headers: Record<string, string> = {}; switch (config.AUTH_STRATEGY) { case 'api_key': if (config.API_KEY) { headers[config.API_KEY_HEADER.toLowerCase()] = config.API_KEY; } break; case 'bearer': if (config.BEARER_TOKEN) { headers.authorization = `Bearer ${config.BEARER_TOKEN}`; } break; case 'custom': Object.assign(headers, parseCustomHeaders(config.CUSTOM_HEADERS)); break; } return headers; } /** * Auth middleware that handles multiple strategies. * * Strategies: * - 'oauth': Map RS token → Provider token (full OAuth flow) * - 'bearer': Use static BEARER_TOKEN from config * - 'api_key': Use static API_KEY in API_KEY_HEADER * - 'custom': Use static CUSTOM_HEADERS * - 'none': No auth, pass through * * After this middleware: * - c.authContext.resolvedHeaders: Headers ready for API calls * - c.authContext.providerToken: Access token (if available) * - c.authContext.provider: Full token info (OAuth only) */ export function createAuthHeaderMiddleware(): MiddlewareHandler<{ Bindings: HttpBindings; }> { const accept = new Set( (config.MCP_ACCEPT_HEADERS as string[]).map((h) => h.toLowerCase()), ); // Always include standard auth headers for (const h of ['authorization', 'x-api-key', 'x-auth-token']) { accept.add(h); } // Pre-compute static headers for non-OAuth strategies const staticHeaders = buildStaticAuthHeaders(); const strategy = config.AUTH_STRATEGY; return async (c, next) => { const incoming = c.req.raw.headers; const forwarded: Record<string, string> = {}; for (const [k, v] of incoming as unknown as Iterable<[string, string]>) { const lower = k.toLowerCase(); if (accept.has(lower)) { forwarded[lower] = v; } } // Initialize auth context const authContext: AuthContext = { strategy, authHeaders: forwarded, resolvedHeaders: { ...forwarded }, }; // Handle based on strategy switch (strategy) { case 'oauth': await handleOAuthStrategy(authContext, forwarded); break; case 'bearer': // Use static bearer token from config authContext.resolvedHeaders = { ...forwarded, ...staticHeaders }; authContext.providerToken = config.BEARER_TOKEN; break; case 'api_key': // Use static API key from config authContext.resolvedHeaders = { ...forwarded, ...staticHeaders }; authContext.providerToken = config.API_KEY; break; case 'custom': // Merge custom headers authContext.resolvedHeaders = { ...forwarded, ...staticHeaders }; break; default: // Pass through as-is (including 'none' strategy) break; } // Attach to context for downstream handlers (c as unknown as { authContext: AuthContext }).authContext = authContext; // Legacy: attach authHeaders for backward compatibility (c as unknown as { authHeaders?: Record<string, string> }).authHeaders = authContext.resolvedHeaders; await next(); }; } /** * Handle OAuth strategy: map RS token to provider token. */ async function handleOAuthStrategy( authContext: AuthContext, forwarded: Record<string, string>, ): Promise<void> { const auth = forwarded.authorization; const bearerMatch = auth?.match(/^\s*Bearer\s+(.+)$/i); const rsToken = bearerMatch?.[1]; if (!rsToken) return; authContext.rsToken = rsToken; try { const store = getTokenStore(); const record = await store.getByRsAccess(rsToken); if (record?.provider?.access_token) { const now = Date.now(); const expiresAt = record.provider.expires_at ?? 0; if (expiresAt && now >= expiresAt - 60_000) { logger.warning('auth_middleware', { message: 'Provider token expired or expiring soon', expiresAt, now, }); } authContext.providerToken = record.provider.access_token; authContext.provider = record.provider; // Replace RS token with provider token in resolved headers authContext.resolvedHeaders.authorization = `Bearer ${record.provider.access_token}`; logger.debug('auth_middleware', { message: 'Mapped RS token to provider token', hasRefreshToken: Boolean(record.provider.refresh_token), expiresAt: record.provider.expires_at, }); } else if (config.AUTH_REQUIRE_RS && !config.AUTH_ALLOW_DIRECT_BEARER) { delete authContext.resolvedHeaders.authorization; logger.warning('auth_middleware', { message: 'RS token not found in store', }); } } catch (error) { logger.error('auth_middleware', { message: 'Failed to look up RS token', error: (error as Error).message, }); } }

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/iceener/spotify-streamable-mcp-server'

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