Skip to main content
Glama

Curupira

by drzln
auth.ts4.82 kB
/** * Authentication module * * Handles JWT authentication for staging/production environments */ import * as jwt from 'jsonwebtoken' import type { FastifyRequest, FastifyReply } from 'fastify' import { logger } from '../config/logger.js' import { z } from 'zod' // JWT payload schema const jwtPayloadSchema = z.object({ sub: z.string(), // Subject (user ID) iat: z.number(), // Issued at exp: z.number(), // Expiration aud: z.string().optional(), // Audience iss: z.string().optional(), // Issuer scope: z.array(z.string()).optional(), // Permissions }) export type JWTPayload = z.infer<typeof jwtPayloadSchema> export interface AuthConfig { enabled: boolean jwtSecret?: string jwtPublicKey?: string issuer?: string audience?: string expiresIn?: string algorithms?: jwt.Algorithm[] } export class AuthManager { private config: AuthConfig private verifyOptions: jwt.VerifyOptions constructor(config: AuthConfig) { this.config = config this.verifyOptions = { algorithms: config.algorithms || ['HS256', 'RS256'], issuer: config.issuer, audience: config.audience, } } /** * Verify JWT token */ async verifyToken(token: string): Promise<JWTPayload> { if (!this.config.enabled) { throw new Error('Authentication is not enabled') } const secret = this.config.jwtPublicKey || this.config.jwtSecret if (!secret) { throw new Error('No JWT secret or public key configured') } try { const decoded = jwt.verify(token, secret, this.verifyOptions) const payload = jwtPayloadSchema.parse(decoded) // Check expiration if (payload.exp && payload.exp < Date.now() / 1000) { throw new Error('Token expired') } return payload } catch (error) { logger.error({ error }, 'JWT verification failed') throw new Error('Invalid token') } } /** * Extract token from request */ extractToken(request: FastifyRequest): string | null { // Check Authorization header const authHeader = request.headers.authorization if (authHeader?.startsWith('Bearer ')) { return authHeader.substring(7) } // Check query parameter (for SSE connections) const query = request.query as Record<string, string> if (query.token) { return query.token } // Check cookie const cookies = (request as any).cookies if (cookies?.token) { return cookies.token } return null } /** * Fastify authentication hook */ async authenticate(request: FastifyRequest, reply: FastifyReply) { if (!this.config.enabled) { // Authentication disabled, allow all requests return } try { const token = this.extractToken(request) if (!token) { reply.code(401).send({ error: 'No authentication token provided' }) return } const payload = await this.verifyToken(token) // Attach user info to request const req = request as any req.user = { id: payload.sub, scope: payload.scope || [], } logger.debug({ userId: payload.sub }, 'Request authenticated') } catch (error) { logger.warn({ error }, 'Authentication failed') reply.code(401).send({ error: 'Authentication failed' }) } } /** * Check if user has required scope */ hasScope(request: FastifyRequest, requiredScope: string): boolean { const req = request as any const user = req.user if (!user || !user.scope) { return false } return user.scope.includes(requiredScope) || user.scope.includes('*') } /** * Generate a token (for testing) */ generateToken(payload: Partial<JWTPayload>): string { if (!this.config.jwtSecret) { throw new Error('No JWT secret configured') } const fullPayload: JWTPayload = { sub: payload.sub || 'test-user', iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour iss: this.config.issuer, aud: this.config.audience, scope: payload.scope || ['read', 'write'], } return jwt.sign(fullPayload, this.config.jwtSecret, { algorithm: 'HS256', }) } /** * Middleware for optional authentication */ async optionalAuth(request: FastifyRequest, reply: FastifyReply) { if (!this.config.enabled) { return } try { const token = this.extractToken(request) if (token) { const payload = await this.verifyToken(token) const req = request as any req.user = { id: payload.sub, scope: payload.scope || [], } } } catch (error) { // Ignore errors for optional auth logger.debug({ error }, 'Optional authentication failed') } } }

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/drzln/curupira'

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