Skip to main content
Glama
auth-middleware.ts10.1 kB
import { Request, Response, NextFunction } from 'express'; /** * OAuth 2.1 MCP Auth Middleware for Smartling * * Implements OAuth 2.1 with PKCE for MCP compliance while maintaining * compatibility with Smartling's existing userIdentifier/userSecret flow. * * This is a hybrid approach that: * 1. Accepts OAuth 2.1 access tokens for MCP clients * 2. Falls back to Smartling credentials for legacy compatibility * 3. Implements Authorization Server Metadata discovery */ export interface SmartlingOAuthConfig { // Smartling API credentials (legacy compatibility) userIdentifier: string; userSecret: string; baseUrl?: string; // OAuth 2.1 MCP configuration enableOAuth?: boolean; clientId?: string; clientSecret?: string; authServerUrl?: string; // Token validation jwtSecret?: string; tokenExpiry?: number; // in seconds, default 3600 } export interface OAuthTokenPayload { sub: string; // subject (user ID) aud: string; // audience (should be our service) iss: string; // issuer exp: number; // expiration time iat: number; // issued at scope?: string; // requested scopes client_id?: string; } export interface AuthContext { userId?: string; clientId?: string; scopes?: string[]; accessToken?: string; // Smartling credentials for API calls smartlingToken?: string; smartlingConfig: { userIdentifier: string; userSecret: string; baseUrl: string; }; } /** * OAuth 2.1 Authorization Server Metadata * Implementation of RFC 8414 */ export function createAuthServerMetadata(config: SmartlingOAuthConfig) { const baseUrl = config.authServerUrl || config.baseUrl || 'https://api.smartling.com'; return { issuer: baseUrl, authorization_endpoint: `${baseUrl}/oauth/authorize`, token_endpoint: `${baseUrl}/oauth/token`, jwks_uri: `${baseUrl}/.well-known/jwks.json`, registration_endpoint: `${baseUrl}/oauth/register`, scopes_supported: [ 'smartling:read', 'smartling:write', 'smartling:admin', 'smartling:files:read', 'smartling:files:write', 'smartling:projects:read', 'smartling:jobs:read', 'smartling:jobs:write' ], response_types_supported: ['code'], grant_types_supported: ['authorization_code', 'client_credentials', 'refresh_token'], token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'], code_challenge_methods_supported: ['S256'], subject_types_supported: ['public'], id_token_signing_alg_values_supported: ['RS256'], userinfo_endpoint: `${baseUrl}/oauth/userinfo`, service_documentation: 'https://github.com/Jacobolevy/smartling-mcp-server', ui_locales_supported: ['en-US', 'es-ES'], }; } /** * Validates OAuth 2.1 Bearer token */ export async function validateBearerToken(token: string, config: SmartlingOAuthConfig): Promise<OAuthTokenPayload | null> { try { // For demo purposes, we'll implement JWT validation // In production, this should validate against your OAuth server if (!token.startsWith('Bearer ')) { return null; } const jwtToken = token.substring(7); // Basic JWT validation (in production, use proper JWT library) const parts = jwtToken.split('.'); if (parts.length !== 3 || !parts[1]) { return null; } const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString()); // Validate expiration if (payload.exp && Date.now() >= payload.exp * 1000) { return null; } // Validate audience (should be our service) if (payload.aud && !payload.aud.includes('smartling-mcp-server')) { return null; } return payload as OAuthTokenPayload; } catch (error) { console.error('Token validation error:', error); return null; } } /** * OAuth middleware for MCP requests */ export function createOAuthMiddleware(config: SmartlingOAuthConfig) { return async (req: Request, res: Response, next: NextFunction) => { try { const authHeader = req.headers.authorization; let authContext: AuthContext = { smartlingConfig: { userIdentifier: config.userIdentifier, userSecret: config.userSecret, baseUrl: config.baseUrl || 'https://api.smartling.com' } }; // Try OAuth 2.1 validation first if enabled if (config.enableOAuth && authHeader) { const tokenPayload = await validateBearerToken(authHeader, config); if (tokenPayload) { authContext.userId = tokenPayload.sub; if (tokenPayload.client_id) { authContext.clientId = tokenPayload.client_id; } authContext.scopes = tokenPayload.scope ? tokenPayload.scope.split(' ') : []; authContext.accessToken = authHeader; // Attach auth context to request (req as any).auth = authContext; return next(); } } // For MCP protocol, if no valid OAuth token, return 401 if (req.path.startsWith('/mcp') || req.headers['user-agent']?.includes('mcp')) { return res.status(401).json({ error: 'unauthorized', error_description: 'Valid OAuth 2.1 access token required', www_authenticate: 'Bearer realm="smartling-mcp-server", scope="smartling:read smartling:write"' }); } // For legacy compatibility, allow requests without OAuth (req as any).auth = authContext; next(); } catch (error) { console.error('OAuth middleware error:', error); res.status(500).json({ error: 'server_error', error_description: 'Internal authentication error' }); } }; } /** * Scope validation middleware */ export function requireScopes(...requiredScopes: string[]) { return (req: Request, res: Response, next: NextFunction): void => { const authContext = (req as any).auth as AuthContext; if (!authContext?.scopes) { res.status(403).json({ error: 'insufficient_scope', error_description: `Required scopes: ${requiredScopes.join(', ')}`, scope: requiredScopes.join(' ') }); return; } const hasRequiredScopes = requiredScopes.every(scope => authContext.scopes?.includes(scope) || authContext.scopes?.includes('smartling:admin') ); if (!hasRequiredScopes) { res.status(403).json({ error: 'insufficient_scope', error_description: `Required scopes: ${requiredScopes.join(', ')}`, scope: requiredScopes.join(' ') }); return; } next(); }; } /** * Express route handlers for OAuth endpoints */ export function createOAuthRoutes(config: SmartlingOAuthConfig) { return { // Authorization Server Metadata (RFC 8414) '/.well-known/oauth-authorization-server': (req: Request, res: Response): void => { res.json(createAuthServerMetadata(config)); }, // OAuth authorization endpoint '/oauth/authorize': (req: Request, res: Response): void => { // In a full implementation, this would redirect to login page // For MCP, we primarily use client credentials flow res.status(501).json({ error: 'not_implemented', error_description: 'Authorization code flow not implemented. Use client credentials flow.' }); }, // OAuth token endpoint '/oauth/token': async (req: Request, res: Response): Promise<void> => { const { grant_type, client_id, client_secret, scope } = req.body; if (grant_type !== 'client_credentials') { res.status(400).json({ error: 'unsupported_grant_type', error_description: 'Only client_credentials grant type is supported' }); return; } if (!client_id || !client_secret) { res.status(400).json({ error: 'invalid_request', error_description: 'client_id and client_secret are required' }); return; } // Validate client credentials if (client_id !== config.clientId || client_secret !== config.clientSecret) { res.status(401).json({ error: 'invalid_client', error_description: 'Invalid client credentials' }); return; } // Generate access token (in production, use proper JWT library) const payload = { sub: client_id, aud: 'smartling-mcp-server', iss: config.authServerUrl || config.baseUrl || 'https://api.smartling.com', exp: Math.floor(Date.now() / 1000) + (config.tokenExpiry || 3600), iat: Math.floor(Date.now() / 1000), scope: scope || 'smartling:read smartling:write', client_id: client_id }; // Simple JWT (in production, use proper signing) const token = Buffer.from(JSON.stringify(payload)).toString('base64'); res.json({ access_token: `Bearer ${token}`, token_type: 'Bearer', expires_in: config.tokenExpiry || 3600, scope: payload.scope }); }, // Dynamic Client Registration (RFC 7591) '/oauth/register': (req: Request, res: Response): void => { const { client_name, redirect_uris, grant_types, scope } = req.body; if (!client_name) { res.status(400).json({ error: 'invalid_request', error_description: 'client_name is required' }); return; } // Generate client credentials const clientId = `mcp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const clientSecret = Math.random().toString(36).substr(2, 32); res.json({ client_id: clientId, client_secret: clientSecret, client_name: client_name, grant_types: grant_types || ['client_credentials'], redirect_uris: redirect_uris || [], scope: scope || 'smartling:read smartling:write', token_endpoint_auth_method: 'client_secret_basic' }); } }; }

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/Jacobolevy/smartling-mcp-server'

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