Skip to main content
Glama
IBM
by IBM
tokenManager.ts7.55 kB
/** * @fileoverview Token management system for IBM i HTTP authentication. * Handles secure token generation, validation, and lifecycle management. * * @module src/ibmi-mcp-server/auth/tokenManager */ import crypto from "crypto"; import { config } from "@/config/index.js"; import { logger, RequestContext, requestContextService, } from "@/utils/index.js"; import type { DaemonServer } from "@ibm/mapepire-js"; /** * IBM i credentials for authenticated sessions - using DaemonServer interface */ export type IBMiCredentials = DaemonServer; /** * Token session metadata */ export interface TokenSession { token: string; credentials: IBMiCredentials; createdAt: Date; expiresAt: Date; lastUsed: Date; } /** * Token generation and validation result */ export interface TokenValidationResult { valid: boolean; session?: TokenSession; error?: string; } /** * Token manager for secure token generation and session management */ export class TokenManager { private static instance: TokenManager; private sessions = new Map<string, TokenSession>(); private cleanupTimer: NodeJS.Timeout | null = null; private constructor() { this.startCleanupTimer(); } /** * Get singleton instance of TokenManager */ static getInstance(): TokenManager { if (!TokenManager.instance) { TokenManager.instance = new TokenManager(); } return TokenManager.instance; } /** * Generate a cryptographically secure bearer token (1024+ bytes) * @param credentials - IBM i credentials for this session * @param expirySeconds - Token lifetime in seconds (optional) * @returns Generated token string */ generateToken( credentials: IBMiCredentials, expirySeconds?: number, context?: RequestContext, ): string { const operationContext = context || requestContextService.createRequestContext({ operation: "generateToken", }); const expiry = expirySeconds || config.ibmiHttpAuth.tokenExpirySeconds; // Generate 1024+ bytes of secure random data const tokenBytes = crypto.randomBytes(256); const token = tokenBytes.toString("base64url"); const now = new Date(); const expiresAt = new Date(now.getTime() + expiry * 1000); const session: TokenSession = { token, credentials, createdAt: now, expiresAt, lastUsed: now, }; this.sessions.set(token, session); logger.info( { ...operationContext, user: credentials.user, host: credentials.host, expirySeconds: expiry, expiresAt: expiresAt.toISOString(), sessionCount: this.sessions.size, }, "Generated new authentication token", ); return token; } /** * Validate a bearer token and return session info * @param token - Bearer token to validate * @returns Validation result with session info if valid */ validateToken( token: string, context?: RequestContext, ): TokenValidationResult { const operationContext = context || requestContextService.createRequestContext({ operation: "validateToken", }); if (!token || typeof token !== "string") { logger.debug({ ...operationContext }, "Invalid token format"); return { valid: false, error: "Invalid token format" }; } const session = this.sessions.get(token); if (!session) { logger.debug( { ...operationContext, tokenPrefix: token.substring(0, 10) + "...", }, "Token not found in session store", ); return { valid: false, error: "Token not found" }; } // Check if token has expired if (new Date() > session.expiresAt) { logger.info( { ...operationContext, user: session.credentials.user, expiredAt: session.expiresAt.toISOString(), }, "Token has expired", ); this.sessions.delete(token); return { valid: false, error: "Token expired" }; } // Update last used timestamp session.lastUsed = new Date(); logger.debug( { ...operationContext, user: session.credentials.user, lastUsed: session.lastUsed.toISOString(), }, "Token validated successfully", ); return { valid: true, session }; } /** * Revoke a specific token * @param token - Token to revoke */ revokeToken(token: string, context?: RequestContext): boolean { const operationContext = context || requestContextService.createRequestContext({ operation: "revokeToken", }); const session = this.sessions.get(token); if (!session) { logger.debug({ ...operationContext }, "Token not found for revocation"); return false; } this.sessions.delete(token); logger.info( { ...operationContext, user: session.credentials.user, sessionCount: this.sessions.size, }, "Token revoked successfully", ); return true; } /** * Get current session statistics */ getSessionStats(): { totalSessions: number; activeSessions: number; expiredSessions: number; } { const now = new Date(); let activeSessions = 0; let expiredSessions = 0; for (const session of this.sessions.values()) { if (now <= session.expiresAt) { activeSessions++; } else { expiredSessions++; } } return { totalSessions: this.sessions.size, activeSessions, expiredSessions, }; } /** * Clean up expired tokens */ private cleanupExpiredTokens(): void { const context = requestContextService.createRequestContext({ operation: "cleanupExpiredTokens", }); const now = new Date(); const expiredTokens: string[] = []; for (const [token, session] of this.sessions.entries()) { if (now > session.expiresAt) { expiredTokens.push(token); } } for (const token of expiredTokens) { this.sessions.delete(token); } if (expiredTokens.length > 0) { logger.info( { ...context, expiredCount: expiredTokens.length, remainingCount: this.sessions.size, }, "Cleaned up expired tokens", ); } } /** * Start the cleanup timer */ private startCleanupTimer(): void { if (!config.ibmiHttpAuth.enabled) { return; } const intervalMs = config.ibmiHttpAuth.cleanupIntervalSeconds * 1000; this.cleanupTimer = setInterval(() => { this.cleanupExpiredTokens(); }, intervalMs); logger.info( { operation: "startCleanupTimer", intervalSeconds: config.ibmiHttpAuth.cleanupIntervalSeconds, }, "Started token cleanup timer", ); } /** * Stop the cleanup timer and clear all sessions */ shutdown(): void { const context = requestContextService.createRequestContext({ operation: "tokenManagerShutdown", }); if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = null; } const sessionCount = this.sessions.size; this.sessions.clear(); logger.info( { ...context, clearedSessions: sessionCount, }, "Token manager shutdown completed", ); } /** * Check if max concurrent sessions limit would be exceeded */ canCreateNewSession(): boolean { return this.sessions.size < config.ibmiHttpAuth.maxConcurrentSessions; } }

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