Skip to main content
Glama
token-manager.ts6.21 kB
import { SignJWT, jwtVerify, exportJWK, JWK } from 'jose'; import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; import { resolve, join } from 'path'; import crypto from 'crypto'; import { ConfigLoader } from '../config/config-loader.js'; import { AuthToken } from '../types.js'; import type { UserManagementInfo } from '../dual-auth.js'; type KeyLike = any; // jose v6 doesn't export KeyLike directly export interface TokenPayload { sub: string; // User subject (email or ID) clientId: string; // Tenant ID umbrellaAuth: AuthToken; // Direct Umbrella auth token (includes userManagementInfo) exp: number; iat: number; } export class TokenManager { private config = ConfigLoader.getInstance(); private privateKey: KeyLike | null = null; private publicKey: KeyLike | null = null; private jwkPublic: JWK | null = null; constructor() { this.initializeRSAKeys(); } private async initializeRSAKeys(): Promise<void> { const authConfig = this.config.getConfig().auth; const keyPath = resolve(authConfig.rsaKeyPath); // Create keys directory if it doesn't exist if (!existsSync(keyPath)) { mkdirSync(keyPath, { recursive: true }); } const privateKeyPath = join(keyPath, 'private.pem'); const publicKeyPath = join(keyPath, 'public.pem'); const jwkPath = join(keyPath, 'jwk.json'); try { // Try to load existing keys if (existsSync(privateKeyPath) && existsSync(publicKeyPath)) { const privateKeyPem = readFileSync(privateKeyPath, 'utf-8'); const publicKeyPem = readFileSync(publicKeyPath, 'utf-8'); this.privateKey = crypto.createPrivateKey(privateKeyPem); this.publicKey = crypto.createPublicKey(publicKeyPem); if (existsSync(jwkPath)) { this.jwkPublic = JSON.parse(readFileSync(jwkPath, 'utf-8')); } else { this.jwkPublic = await exportJWK(this.publicKey); this.jwkPublic.kid = this.generateKid(this.publicKey); writeFileSync(jwkPath, JSON.stringify(this.jwkPublic, null, 2)); } console.log('[TOKEN] Loaded existing RSA keys'); } else { // Generate new keys await this.generateAndSaveKeys(privateKeyPath, publicKeyPath, jwkPath); } } catch (error) { console.error('[TOKEN] Error loading keys, generating new ones:', error); await this.generateAndSaveKeys(privateKeyPath, publicKeyPath, jwkPath); } } private async generateAndSaveKeys(privateKeyPath: string, publicKeyPath: string, jwkPath: string): Promise<void> { console.log('[TOKEN] Generating new RSA key pair...'); // Use Node's crypto to generate RSA keys const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }); // Store as strings first const privateKeyPem = privateKey; const publicKeyPem = publicKey; // Save to files writeFileSync(privateKeyPath, privateKeyPem); writeFileSync(publicKeyPath, publicKeyPem); // Convert to KeyLike for jose this.privateKey = crypto.createPrivateKey(privateKeyPem); this.publicKey = crypto.createPublicKey(publicKeyPem); // Export and save JWK this.jwkPublic = await exportJWK(this.publicKey); this.jwkPublic.kid = this.generateKid(this.publicKey); writeFileSync(jwkPath, JSON.stringify(this.jwkPublic, null, 2)); console.log('[TOKEN] RSA keys generated and saved'); } private generateKid(publicKey: KeyLike): string { // Generate a deterministic key ID from the public key const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }) as string; return crypto.createHash('sha256').update(publicKeyPem).digest('hex').substring(0, 8); } /** * Create a JWT containing Umbrella auth token directly (no encryption) */ async createToken(userEmail: string, clientId: string, umbrellaAuth: AuthToken): Promise<string> { if (!this.privateKey) { await this.initializeRSAKeys(); } const authConfig = this.config.getConfig().auth; const now = Math.floor(Date.now() / 1000); const exp = now + authConfig.tokenExpiration; // Simply embed the Umbrella auth token directly in the JWT (including userManagementInfo) const token = await new SignJWT({ sub: userEmail, clientId, umbrellaAuth: { Authorization: umbrellaAuth.Authorization, userManagementInfo: umbrellaAuth.userManagementInfo } }) .setProtectedHeader({ alg: 'RS256', kid: this.jwkPublic?.kid || 'default' }) .setIssuer(authConfig.issuer) .setIssuedAt(now) .setExpirationTime(exp) .sign(this.privateKey!); if (this.config.isDebugMode()) { console.log('[TOKEN] Created token for:', { userEmail, clientId, expiresIn: `${authConfig.tokenExpiration}s` }); } return token; } /** * Verify token and extract Umbrella credentials directly */ async verifyToken(token: string): Promise<{ payload: TokenPayload; umbrellaAuth: AuthToken }> { if (!this.publicKey) { await this.initializeRSAKeys(); } const authConfig = this.config.getConfig().auth; const { payload } = await jwtVerify(token, this.publicKey!, { issuer: authConfig.issuer, algorithms: ['RS256'] }); const tokenPayload = payload as unknown as TokenPayload; // Extract Umbrella auth directly (no decryption needed), including userManagementInfo const umbrellaAuth: AuthToken = { Authorization: tokenPayload.umbrellaAuth.Authorization, userManagementInfo: tokenPayload.umbrellaAuth.userManagementInfo }; return { payload: tokenPayload, umbrellaAuth }; } /** * Check if token is expired */ isTokenExpired(exp: number): boolean { const now = Math.floor(Date.now() / 1000); return now >= exp; } /** * Get JWK for public key (used by JWKS endpoint) */ getPublicJWK(): JWK | null { return this.jwkPublic; } }

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/daviddraiumbrella/invoice-monitoring'

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