Skip to main content
Glama
IBM
by IBM
httpAuthEndpoint.ts9.99 kB
/** * @fileoverview HTTP authentication endpoint implementation for IBM i authentication. * Provides POST /api/v1/auth endpoint with TLS enforcement and bearer token generation. * * @module src/ibmi-mcp-server/auth/httpAuthEndpoint */ import { Context } from "hono"; import { config } from "@/config/index.js"; import { logger, requestContextService } from "@/utils/index.js"; import { JsonRpcErrorCode, McpError } from "@/types-global/errors.js"; import { TokenManager } from "./tokenManager.js"; import { AuthenticatedPoolManager } from "../services/authenticatedPoolManager.js"; import { type AuthRequest, type AuthResponse, type AuthCredentials, type EncryptedAuthEnvelope, } from "./types.js"; import { decryptAuthEnvelope } from "./crypto.js"; /** * Validate authentication request body * @param body - Request body to validate * @returns Validated request or throws error */ function validateAuthRequest(body: unknown): AuthRequest { if (!body || typeof body !== "object") { throw new McpError( JsonRpcErrorCode.InvalidRequest, "Request body must be a JSON object", ); } const request = body as Record<string, unknown>; const validated: Partial<AuthRequest> = {}; // Validate host (required) if ( !request.host || typeof request.host !== "string" || request.host.trim().length === 0 ) { throw new McpError( JsonRpcErrorCode.InvalidRequest, "Host is required and must be a non-empty string", ); } validated.host = request.host.trim(); // Validate duration if (request.duration !== undefined) { if ( typeof request.duration !== "number" || request.duration <= 0 || request.duration > 86400 ) { throw new McpError( JsonRpcErrorCode.InvalidRequest, "Duration must be a positive number not exceeding 86400 seconds (24 hours)", ); } validated.duration = request.duration; } // Validate poolstart if (request.poolstart !== undefined) { if ( typeof request.poolstart !== "number" || request.poolstart < 1 || request.poolstart > 50 ) { throw new McpError( JsonRpcErrorCode.InvalidRequest, "poolstart must be a number between 1 and 50", ); } validated.poolstart = request.poolstart; } // Validate poolmax if (request.poolmax !== undefined) { if ( typeof request.poolmax !== "number" || request.poolmax < 1 || request.poolmax > 100 ) { throw new McpError( JsonRpcErrorCode.InvalidRequest, "poolmax must be a number between 1 and 100", ); } validated.poolmax = request.poolmax; } // Validate poolstart <= poolmax if ( validated.poolstart && validated.poolmax && validated.poolstart > validated.poolmax ) { throw new McpError( JsonRpcErrorCode.InvalidRequest, "poolstart cannot be greater than poolmax", ); } return validated as AuthRequest; } function validateEncryptedEnvelope(body: unknown): EncryptedAuthEnvelope { if (!body || typeof body !== "object") { throw new McpError( JsonRpcErrorCode.InvalidRequest, "Encrypted payload must be a JSON object", ); } const envelope = body as Record<string, unknown>; const requiredFields: Array<keyof EncryptedAuthEnvelope> = [ "keyId", "encryptedSessionKey", "iv", "authTag", "ciphertext", ]; for (const field of requiredFields) { const value = envelope[field]; if (typeof value !== "string" || value.trim().length === 0) { throw new McpError( JsonRpcErrorCode.InvalidRequest, `Encrypted payload is missing required field: ${field}`, ); } } return { keyId: envelope.keyId as string, encryptedSessionKey: envelope.encryptedSessionKey as string, iv: envelope.iv as string, authTag: envelope.authTag as string, ciphertext: envelope.ciphertext as string, }; } function validateCredentials(credentials: AuthCredentials): AuthCredentials { if (!credentials.username || !credentials.username.trim()) { throw new McpError(JsonRpcErrorCode.InvalidRequest, "Username is required"); } if (credentials.password === undefined || credentials.password === null) { throw new McpError(JsonRpcErrorCode.InvalidRequest, "Password is required"); } return { username: credentials.username.trim(), password: credentials.password, }; } /** * Middleware to enforce TLS for authentication endpoints * In development environment, TLS can be bypassed with IBMI_AUTH_ALLOW_HTTP=true */ export const enforceTLS = async (c: Context, next: () => Promise<void>) => { const protocol = c.req.header("x-forwarded-proto") || (c.req.url.startsWith("https:") ? "https" : "http"); // Allow HTTP in development if explicitly configured const allowHttp = config.environment === "development" && config.ibmiHttpAuth.allowHttp; if (protocol !== "https" && !allowHttp) { const context = requestContextService.createRequestContext({ operation: "enforceTLS", protocol, url: c.req.url, }); logger.warning( { ...context, clientIp: c.req.header("x-forwarded-for") || c.req.header("x-real-ip") || "unknown", allowHttp, environment: config.environment, }, "TLS required for authentication endpoint", ); throw new McpError( JsonRpcErrorCode.InvalidRequest, "HTTPS/TLS is required for authentication endpoints", ); } if (allowHttp && protocol === "http") { logger.warning( { operation: "enforceTLS", protocol, environment: config.environment, }, "Allowing HTTP for authentication endpoint in development mode", ); } await next(); }; /** * Handle POST /api/v1/auth authentication requests */ export const handleAuthRequest = async (c: Context) => { const context = requestContextService.createRequestContext({ operation: "handleAuthRequest", method: c.req.method, path: c.req.path, }); try { // Check if IBM i HTTP auth is enabled if (!config.ibmiHttpAuth.enabled) { throw new McpError( JsonRpcErrorCode.MethodNotFound, "IBM i HTTP authentication is not enabled on this server", ); } let envelope: EncryptedAuthEnvelope; try { const body = await c.req.json(); envelope = validateEncryptedEnvelope(body); } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( JsonRpcErrorCode.InvalidRequest, "Invalid JSON in request body", ); } const decrypted = decryptAuthEnvelope(envelope, context); const credentials = validateCredentials(decrypted.credentials); const requestBody = validateAuthRequest(decrypted.request); logger.info( { ...context, user: credentials.username, }, "Processing authentication request", ); // Check concurrent session limits const tokenManager = TokenManager.getInstance(); if (!tokenManager.canCreateNewSession()) { throw new McpError( JsonRpcErrorCode.InternalError, "Maximum concurrent sessions reached. Please try again later", ); } // Create IBM i credentials as DaemonServer object using existing config defaults // Default to ignore unauthorized SSL (like existing connectionPool.ts) const ignoreUnauthorized = config.db2i?.ignoreUnauthorized ?? true; const ibmiCredentials = { host: requestBody.host, user: credentials.username, password: credentials.password, rejectUnauthorized: !ignoreUnauthorized, // Use existing Db2i config }; // Generate authentication token const token = tokenManager.generateToken( ibmiCredentials, requestBody.duration, context, ); // Create authenticated pool const poolManager = AuthenticatedPoolManager.getInstance(); await poolManager.createPool( token, ibmiCredentials, { startingSize: requestBody.poolstart || 2, maxSize: requestBody.poolmax || 10, }, context, ); // Calculate expiration const expirySeconds = requestBody.duration || config.ibmiHttpAuth.tokenExpirySeconds; const expiresAt = new Date(Date.now() + expirySeconds * 1000); const response: AuthResponse = { access_token: token, token_type: "Bearer", expires_in: expirySeconds, expires_at: expiresAt.toISOString(), }; logger.info( { ...context, user: credentials.username, expiresAt: expiresAt.toISOString(), poolStartingSize: requestBody.poolstart || 2, poolMaxSize: requestBody.poolmax || 10, }, "Authentication successful, token generated", ); return c.json(response, 201); } catch (error) { if (error instanceof McpError) { logger.warning( { ...context, errorCode: error.code, errorMessage: error.message, }, "Authentication request failed", ); const status = error.code === JsonRpcErrorCode.InvalidRequest ? 400 : error.code === JsonRpcErrorCode.MethodNotFound ? 404 : error.code === JsonRpcErrorCode.Unauthorized ? 401 : 500; return c.json( { error: { code: error.code, message: error.message, }, }, status, ); } logger.error( { ...context, error: error instanceof Error ? error.message : String(error), }, "Unexpected error in authentication request", ); return c.json( { error: { code: JsonRpcErrorCode.InternalError, message: "Internal server error", }, }, 500, ); } };

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/IBM/ibmi-mcp'

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