Skip to main content
Glama
middleware.ts12.9 kB
/** * db-mcp - OAuth Middleware * * Express middleware for OAuth 2.0 authentication and authorization. * Extracts Bearer tokens, validates them, and enforces scope requirements. */ import type { Request, Response, NextFunction, RequestHandler } from 'express'; import type { TokenClaims } from './types.js'; import type { TokenValidator } from './TokenValidator.js'; import type { OAuthResourceServer } from './OAuthResourceServer.js'; import { TokenMissingError, InvalidTokenError, InsufficientScopeError, isOAuthError } from './errors.js'; import { scopesGrantToolAccess } from './scopes.js'; import { createModuleLogger, ERROR_CODES } from '../utils/logger.js'; const logger = createModuleLogger('AUTH'); // ============================================================================= // Express Type Extensions // ============================================================================= /** * Extended Express Request with auth context */ declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Express { interface Request { /** Authenticated user claims */ auth?: TokenClaims; /** Raw access token */ accessToken?: string; /** Request ID for tracing */ requestId?: string; } } } // ============================================================================= // Configuration // ============================================================================= /** * Auth middleware configuration */ export interface AuthMiddlewareConfig { /** Token validator instance */ tokenValidator: TokenValidator; /** Resource server instance (for WWW-Authenticate header) */ resourceServer: OAuthResourceServer; /** Paths that bypass authentication (e.g., '/.well-known/*', '/health') */ publicPaths?: string[]; } // ============================================================================= // Token Extraction // ============================================================================= /** * Extract Bearer token from Authorization header * * @param authHeader - Authorization header value * @returns The token or null if not present/invalid */ export function extractBearerToken(authHeader: string | undefined): string | null { if (!authHeader) { return null; } // Check for Bearer scheme (case-insensitive) const parts = authHeader.split(' '); const scheme = parts[0]; const tokenPart = parts[1]; if (parts.length !== 2 || scheme === undefined || scheme.toLowerCase() !== 'bearer') { return null; } if (tokenPart === undefined) { return null; } const token = tokenPart.trim(); return token.length > 0 ? token : null; } // ============================================================================= // Path Matching // ============================================================================= /** * Check if a path matches any of the public path patterns * * Supports: * - Exact matches: '/health' matches '/health' * - Wildcard suffix: '/api/*' matches '/api/users', '/api/posts/1' * - Well-known paths are always public */ function isPublicPath(path: string, publicPaths: string[]): boolean { // Well-known paths are always public (RFC requirement) if (path.startsWith('/.well-known/')) { return true; } for (const pattern of publicPaths) { // Exact match if (pattern === path) { return true; } // Wildcard match if (pattern.endsWith('/*')) { const prefix = pattern.slice(0, -2); if (path === prefix || path.startsWith(prefix + '/')) { return true; } } } return false; } // ============================================================================= // Main Authentication Middleware // ============================================================================= /** * Create the main authentication middleware * * This middleware: * 1. Skips authentication for public paths (e.g., /.well-known/*) * 2. Extracts Bearer token from Authorization header * 3. Validates the token using the TokenValidator * 4. Attaches validated claims to req.auth * 5. Returns 401 with WWW-Authenticate header on failure */ export function createAuthMiddleware(config: AuthMiddlewareConfig): RequestHandler { const { tokenValidator, resourceServer, publicPaths = [] } = config; return async (req: Request, res: Response, next: NextFunction): Promise<void> => { // Generate request ID for tracing const requestId = crypto.randomUUID(); req.requestId = requestId; // Check if path is public if (isPublicPath(req.path, publicPaths)) { logger.info('PUBLIC_PATH', `Public path accessed: ${req.path}`, { context: { requestId, path: req.path } }); next(); return; } // Extract Bearer token const token = extractBearerToken(req.headers.authorization); if (!token) { const error = new TokenMissingError(resourceServer.getResourceUri()); logger.warning( ERROR_CODES.AUTH.TOKEN_MISSING, 'No access token provided', { context: { requestId, path: req.path } } ); res.status(error.httpStatus); res.setHeader('WWW-Authenticate', error.wwwAuthenticate ?? ''); res.json({ error: 'unauthorized', error_description: error.message }); return; } // Validate token const result = await tokenValidator.validate(token); if (!result.valid) { // Create error for logging (variable intentionally used only for type check) new InvalidTokenError(result.error); logger.warning( result.errorCode ?? ERROR_CODES.AUTH.TOKEN_INVALID.full, `Token validation failed: ${result.error ?? 'Unknown error'}`, { context: { requestId, path: req.path } } ); res.status(401); res.setHeader('WWW-Authenticate', resourceServer.getWWWAuthenticateHeader( 'invalid_token', result.error )); res.json({ error: 'invalid_token', error_description: result.error }); return; } // Attach claims to request (claims is guaranteed defined when valid is true) const claims = result.claims; if (!claims) { // Should not happen when valid is true, but satisfies TypeScript res.status(500).json({ error: 'internal_error' }); return; } req.auth = claims; req.accessToken = token; logger.info('AUTH_SUCCESS', `Request authenticated: ${claims.sub}`, { context: { requestId, sub: claims.sub, scopes: claims.scopes.length, path: req.path } }); next(); }; } // ============================================================================= // Scope Enforcement Middleware // ============================================================================= /** * Middleware factory that requires a specific scope * * @param scope - Required scope * @returns Express middleware */ export function requireScope(scope: string): RequestHandler { return (req: Request, res: Response, next: NextFunction): void => { if (!req.auth) { // Should not happen if auth middleware is applied first res.status(401).json({ error: 'unauthorized', error_description: 'Authentication required' }); return; } const hasScope = req.auth.scopes.includes(scope) || req.auth.scopes.includes('admin'); // Admin scope grants all if (!hasScope) { const error = new InsufficientScopeError(scope, req.auth.scopes); logger.warning( ERROR_CODES.AUTH.SCOPE_DENIED, `Insufficient scope: required ${scope}`, { context: { requestId: req.requestId, requiredScope: scope, providedScopes: req.auth.scopes } } ); res.status(error.httpStatus); res.setHeader('WWW-Authenticate', error.wwwAuthenticate ?? ''); res.json({ error: 'insufficient_scope', error_description: error.message, required_scope: scope }); return; } next(); }; } /** * Middleware factory that requires any of the specified scopes * * @param scopes - Array of acceptable scopes (user must have at least one) * @returns Express middleware */ export function requireAnyScope(scopes: string[]): RequestHandler { return (req: Request, res: Response, next: NextFunction): void => { if (!req.auth) { res.status(401).json({ error: 'unauthorized', error_description: 'Authentication required' }); return; } // Admin scope grants all if (req.auth.scopes.includes('admin')) { next(); return; } const hasAnyScope = scopes.some(scope => req.auth?.scopes.includes(scope)); if (!hasAnyScope) { const error = new InsufficientScopeError(scopes, req.auth.scopes); logger.warning( ERROR_CODES.AUTH.SCOPE_DENIED, `Insufficient scope: required one of [${scopes.join(', ')}]`, { context: { requestId: req.requestId, requiredScopes: scopes, providedScopes: req.auth.scopes } } ); res.status(error.httpStatus); res.setHeader('WWW-Authenticate', error.wwwAuthenticate ?? ''); res.json({ error: 'insufficient_scope', error_description: error.message, required_scopes: scopes }); return; } next(); }; } /** * Middleware factory that requires scope for a specific tool * * @param toolName - Name of the tool being accessed * @returns Express middleware */ export function requireToolScope(toolName: string): RequestHandler { return (req: Request, res: Response, next: NextFunction): void => { if (!req.auth) { res.status(401).json({ error: 'unauthorized', error_description: 'Authentication required' }); return; } const hasAccess = scopesGrantToolAccess(req.auth.scopes, toolName); if (!hasAccess) { const error = new InsufficientScopeError( `Tool access: ${toolName}`, req.auth.scopes ); logger.warning( ERROR_CODES.AUTH.SCOPE_DENIED, `Insufficient scope for tool: ${toolName}`, { context: { requestId: req.requestId, toolName, providedScopes: req.auth.scopes } } ); res.status(error.httpStatus); res.setHeader('WWW-Authenticate', error.wwwAuthenticate ?? ''); res.json({ error: 'insufficient_scope', error_description: `Access to tool '${toolName}' denied`, tool: toolName }); return; } next(); }; } // ============================================================================= // Error Handler // ============================================================================= /** * Error handler middleware for OAuth errors * * Should be added after all routes to catch OAuth-related errors */ export function oauthErrorHandler( error: Error, _req: Request, res: Response, next: NextFunction ): void { if (isOAuthError(error)) { res.status(error.httpStatus); if (error.wwwAuthenticate) { res.setHeader('WWW-Authenticate', error.wwwAuthenticate); } res.json({ error: error.code, error_description: error.message }); return; } // Pass to next error handler next(error); }

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