UNPKG

@fastmcp-oauth/kerberos-delegation

Version:

Kerberos delegation module for FastMCP OAuth framework - Windows Kerberos Constrained Delegation support

851 lines (849 loc) 29.5 kB
// src/kerberos-client.ts import kerberos from "kerberos"; var KerberosClient = class { config; serviceTicket; kerberosClient; constructor(config) { this.config = config; } /** * Obtain service ticket (TGT) for MCP Server service account * * This ticket is used to perform S4U2Self operations. * * @throws {Error} If ticket acquisition fails */ async obtainServiceTicket() { console.log("\n[KERBEROS-CLIENT] obtainServiceTicket() called"); try { const servicePrincipal = `${this.config.servicePrincipalName}@${this.config.realm}`; const username = this.config.serviceAccount.username; const realm = this.config.realm; console.log("[KERBEROS-CLIENT] Service principal:", servicePrincipal); console.log("[KERBEROS-CLIENT] Service account:", username); console.log("[KERBEROS-CLIENT] Realm:", realm); console.log("[KERBEROS-CLIENT] Domain Controller:", this.config.domainController); console.log("[KERBEROS-CLIENT] KDC:", this.config.kdc); let authOptions = {}; if (this.config.serviceAccount.keytabPath) { console.log("[KERBEROS-CLIENT] Using keytab authentication:", this.config.serviceAccount.keytabPath); authOptions = { principal: `${username}@${realm}`, keytab: this.config.serviceAccount.keytabPath }; } else if (this.config.serviceAccount.password) { console.log("[KERBEROS-CLIENT] Password provided - attempting password authentication"); console.log("[KERBEROS-CLIENT] NOTE: On Windows, password auth requires credentials in Windows Credential Manager"); authOptions = { principal: `${username}@${realm}`, password: this.config.serviceAccount.password }; } else { console.log("[KERBEROS-CLIENT] No credentials provided - using current user credentials (Windows SSPI)"); console.log("[KERBEROS-CLIENT] This requires the current user to be logged into the domain"); authOptions = {}; } console.log("[KERBEROS-CLIENT] Initializing Kerberos client with node-kerberos library"); console.log("[KERBEROS-CLIENT] Auth options:", { principal: authOptions.principal, hasPassword: !!authOptions.password, hasKeytab: !!authOptions.keytab }); console.log("[KERBEROS-CLIENT] Initializing client for service principal:", servicePrincipal); this.kerberosClient = await kerberos.initializeClient( servicePrincipal, authOptions ); console.log("[KERBEROS-CLIENT] \u2713 Kerberos client initialized"); console.log("[KERBEROS-CLIENT] Obtaining TGT (Ticket Granting Ticket)"); const ticket = await this.kerberosClient.step(""); console.log("[KERBEROS-CLIENT] \u2713 TGT obtained"); console.log("[KERBEROS-CLIENT] Ticket length:", ticket?.length || 0); this.serviceTicket = { principal: servicePrincipal, service: `krbtgt/${realm}@${realm}`, expiresAt: new Date(Date.now() + 10 * 60 * 60 * 1e3), // Default 10 hours ticketData: ticket, flags: ["FORWARDABLE", "RENEWABLE"] }; console.log("[KERBEROS-CLIENT] \u2713 Service ticket stored successfully"); } catch (error) { console.error("[KERBEROS-CLIENT] \u2717 Failed to obtain service ticket:", error); console.error("[KERBEROS-CLIENT] Error details:", { message: error instanceof Error ? error.message : "Unknown error", stack: error instanceof Error ? error.stack : void 0 }); throw new Error( `Failed to obtain service ticket: ${error instanceof Error ? error.message : "Unknown error"}` ); } } /** * Perform S4U2Self - Obtain ticket on behalf of user * * Service for User to Self (S4U2Self) allows a service to obtain a * Kerberos ticket on behalf of a user without requiring the user's * credentials. This is protocol transition. * * Prerequisites: * - Service account has TrustedToAuthForDelegation enabled * - Service has valid TGT (call obtainServiceTicket first) * * NOTE: The node-kerberos library (v2.2.2) does NOT support S4U2Self/S4U2Proxy. * This implementation creates a stub ticket that will be used by Windows * authentication when accessing resources (SMB shares, etc). * * @param userPrincipal - User principal name (e.g., ALICE@COMPANY.COM) * @returns Kerberos ticket for the user (stub implementation) * @throws {Error} If validation fails */ async performS4U2Self(userPrincipal) { console.log("\n[KERBEROS-CLIENT] performS4U2Self() called"); console.log("[KERBEROS-CLIENT] User principal:", userPrincipal); console.log("[KERBEROS-CLIENT] NOTE: Using stub implementation - node-kerberos does not support S4U2Self"); if (!this.serviceTicket) { console.error("[KERBEROS-CLIENT] Service ticket not available"); throw new Error( "Service ticket not obtained. Call obtainServiceTicket() first." ); } try { if (!userPrincipal.includes("@")) { console.error("[KERBEROS-CLIENT] Invalid user principal format:", userPrincipal); throw new Error( `Invalid user principal format: ${userPrincipal}. Expected format: USER@REALM` ); } const [username] = userPrincipal.split("@"); console.log("[KERBEROS-CLIENT] Username:", username); const targetSPN = `${this.config.servicePrincipalName}@${this.config.realm}`; console.log("[KERBEROS-CLIENT] Creating stub ticket for user:", userPrincipal); console.log("[KERBEROS-CLIENT] Target SPN:", targetSPN); console.log("[KERBEROS-CLIENT] Actual delegation will be handled by Windows SSPI"); return { principal: userPrincipal, service: targetSPN, expiresAt: new Date(Date.now() + 10 * 60 * 60 * 1e3), // 10 hours ticketData: Buffer.from(JSON.stringify({ userPrincipal, targetSPN, timestamp: Date.now(), note: "Stub ticket - real delegation handled by Windows SSPI" })).toString("base64"), flags: ["FORWARDABLE", "PROXIABLE", "STUB"] }; } catch (error) { throw new Error( `S4U2Self failed for ${userPrincipal}: ${error instanceof Error ? error.message : "Unknown error"}` ); } } /** * Perform S4U2Proxy - Delegate to backend service * * Service for User to Proxy (S4U2Proxy) allows a service to obtain a * Kerberos ticket for a backend service on behalf of a user. * * Prerequisites: * - User ticket from S4U2Self * - Target SPN in msDS-AllowedToDelegateTo list * * NOTE: The node-kerberos library (v2.2.2) does NOT support S4U2Proxy. * This implementation creates a stub ticket. Actual delegation to backend * services must be handled by Windows SSPI or PowerShell with CredSSP. * * @param userTicket - User ticket from S4U2Self * @param targetSPN - Target service principal (e.g., MSSQLSvc/sql01.company.com:1433 or cifs/fileserver) * @returns Proxy ticket for backend service (stub implementation) * @throws {Error} If validation fails or SPN not allowed */ async performS4U2Proxy(userTicket, targetSPN) { console.log("\n[KERBEROS-CLIENT] performS4U2Proxy() called"); console.log("[KERBEROS-CLIENT] Target SPN:", targetSPN); console.log("[KERBEROS-CLIENT] User principal:", userTicket.principal); console.log("[KERBEROS-CLIENT] NOTE: Using stub implementation - node-kerberos does not support S4U2Proxy"); if (this.config.allowedDelegationTargets && !this.config.allowedDelegationTargets.includes(targetSPN)) { throw new Error( `Target SPN not in allowed delegation targets: ${targetSPN}. Allowed targets: ${this.config.allowedDelegationTargets.join(", ")}` ); } try { const fullTargetSPN = targetSPN.includes("@") ? targetSPN : `${targetSPN}@${this.config.realm}`; console.log("[KERBEROS-CLIENT] Full target SPN:", fullTargetSPN); console.log("[KERBEROS-CLIENT] Creating stub proxy ticket"); console.log("[KERBEROS-CLIENT] Actual delegation will be handled by Windows SSPI"); return { principal: userTicket.principal, service: this.config.servicePrincipalName, targetService: fullTargetSPN, delegatedFrom: `${this.config.serviceAccount.username}@${this.config.realm}`, expiresAt: new Date(Date.now() + 10 * 60 * 60 * 1e3), // 10 hours ticketData: Buffer.from(JSON.stringify({ userPrincipal: userTicket.principal, targetSPN: fullTargetSPN, evidenceTicket: userTicket.ticketData, delegatedFrom: `${this.config.serviceAccount.username}@${this.config.realm}`, timestamp: Date.now(), note: "Stub proxy ticket - real delegation handled by Windows SSPI" })).toString("base64"), flags: ["FORWARDED", "STUB"] }; } catch (error) { throw new Error( `S4U2Proxy failed for target ${targetSPN}: ${error instanceof Error ? error.message : "Unknown error"}` ); } } /** * Validate ticket is still valid * * Checks: * - Ticket has not expired * - Ticket data is not corrupted * * @param ticket - Ticket to validate * @returns true if valid, false otherwise */ async validateTicket(ticket) { try { if (ticket.expiresAt < /* @__PURE__ */ new Date()) { return false; } if (!ticket.ticketData || ticket.ticketData.length === 0) { return false; } Buffer.from(ticket.ticketData, "base64"); return true; } catch { return false; } } /** * Renew ticket before expiration * * Requests a new ticket with extended lifetime. * * @param ticket - Ticket to renew * @returns Renewed ticket * @throws {Error} If renewal fails */ async renewTicket(ticket) { try { if (ticket.service.startsWith("krbtgt/")) { await this.obtainServiceTicket(); return this.serviceTicket; } if (ticket.principal !== this.serviceTicket?.principal) { return await this.performS4U2Self(ticket.principal); } throw new Error("Cannot renew this ticket type"); } catch (error) { throw new Error( `Ticket renewal failed: ${error instanceof Error ? error.message : "Unknown error"}` ); } } /** * Health check - verify can communicate with KDC * * @returns true if KDC is reachable, false otherwise */ async healthCheck() { try { await this.obtainServiceTicket(); return true; } catch { return false; } } /** * Destroy Kerberos client and clear tickets */ async destroy() { this.serviceTicket = void 0; this.kerberosClient = void 0; } }; // src/ticket-cache.ts var TicketCache = class { sessions = /* @__PURE__ */ new Map(); config; metrics = { hits: 0, misses: 0, expired: 0 }; cleanupInterval; constructor(config = {}) { this.config = { enabled: config.enabled ?? true, ttlSeconds: config.ttlSeconds ?? 3600, // 1 hour default renewThresholdSeconds: config.renewThresholdSeconds ?? 300, // 5 minutes maxEntriesPerSession: config.maxEntriesPerSession ?? 10, sessionTimeoutMs: config.sessionTimeoutMs ?? 15 * 60 * 1e3 // 15 minutes }; if (this.config.enabled) { this.startCleanup(); } } /** * Set (cache) a ticket for a session * * @param sessionId - User session ID * @param principal - User principal name (cache key) * @param ticket - Kerberos ticket to cache */ async set(sessionId, principal, ticket) { if (!this.config.enabled) { return; } let session = this.sessions.get(sessionId); if (!session) { session = { sessionId, tickets: /* @__PURE__ */ new Map(), lastActivity: /* @__PURE__ */ new Date() }; this.sessions.set(sessionId, session); } if (this.config.maxEntriesPerSession && session.tickets.size >= this.config.maxEntriesPerSession && !session.tickets.has(principal)) { const oldestKey = this.findOldestTicket(session); if (oldestKey) { session.tickets.delete(oldestKey); } } session.tickets.set(principal, { ticket, cachedAt: /* @__PURE__ */ new Date(), lastAccess: /* @__PURE__ */ new Date(), hitCount: 0 }); session.lastActivity = /* @__PURE__ */ new Date(); } /** * Get (retrieve) a cached ticket * * Returns undefined if: * - Cache is disabled * - Ticket not found * - Ticket expired * - Session expired * * @param sessionId - User session ID * @param principal - User principal name (cache key) * @returns Cached ticket or undefined */ async get(sessionId, principal) { if (!this.config.enabled) { this.metrics.misses++; return void 0; } const session = this.sessions.get(sessionId); if (!session) { this.metrics.misses++; return void 0; } const cached = session.tickets.get(principal); if (!cached) { this.metrics.misses++; return void 0; } const now = /* @__PURE__ */ new Date(); const cacheAge = now.getTime() - cached.cachedAt.getTime(); const ttlMs = this.config.ttlSeconds * 1e3; if (cacheAge > ttlMs) { session.tickets.delete(principal); this.metrics.expired++; this.metrics.misses++; return void 0; } if (cached.ticket.expiresAt < now) { session.tickets.delete(principal); this.metrics.expired++; this.metrics.misses++; return void 0; } cached.lastAccess = now; cached.hitCount++; session.lastActivity = now; this.metrics.hits++; return cached.ticket; } /** * Check if ticket needs renewal soon * * Returns true if ticket will expire within renewThresholdSeconds * * @param sessionId - User session ID * @param principal - User principal name * @returns true if renewal needed, false otherwise */ async needsRenewal(sessionId, principal) { const ticket = await this.get(sessionId, principal); if (!ticket) { return true; } const now = /* @__PURE__ */ new Date(); const timeUntilExpiry = ticket.expiresAt.getTime() - now.getTime(); const renewalThresholdMs = this.config.renewThresholdSeconds * 1e3; return timeUntilExpiry < renewalThresholdMs; } /** * Delete (clear) all tickets for a session * * Call this when user logs out or session ends * * @param sessionId - User session ID */ async delete(sessionId) { this.sessions.delete(sessionId); } /** * Update last activity timestamp for a session * * Call this on every request to prevent session timeout * * @param sessionId - User session ID */ async heartbeat(sessionId) { const session = this.sessions.get(sessionId); if (session) { session.lastActivity = /* @__PURE__ */ new Date(); } } /** * Get cache metrics * * @returns Current cache metrics */ getMetrics() { let totalTickets = 0; let totalAge = 0; let memoryEstimate = 0; const now = /* @__PURE__ */ new Date(); for (const session of this.sessions.values()) { for (const cached of session.tickets.values()) { totalTickets++; totalAge += now.getTime() - cached.cachedAt.getTime(); memoryEstimate += 1050; } memoryEstimate += 200; } return { cacheHits: this.metrics.hits, cacheMisses: this.metrics.misses, expiredTickets: this.metrics.expired, activeSessions: this.sessions.size, totalTickets, averageTicketAge: totalTickets > 0 ? totalAge / totalTickets : 0, memoryUsageEstimate: memoryEstimate }; } /** * Cleanup expired tickets and sessions * * Removes: * - Expired tickets (based on TTL and ticket expiration) * - Inactive sessions (based on sessionTimeoutMs) */ async cleanup() { const now = /* @__PURE__ */ new Date(); const ttlMs = this.config.ttlSeconds * 1e3; const sessionTimeoutMs = this.config.sessionTimeoutMs || 15 * 60 * 1e3; let removedTickets = 0; let removedSessions = 0; for (const [sessionId, session] of this.sessions.entries()) { const sessionAge = now.getTime() - session.lastActivity.getTime(); if (sessionAge > sessionTimeoutMs) { this.sessions.delete(sessionId); removedSessions++; removedTickets += session.tickets.size; continue; } for (const [principal, cached] of session.tickets.entries()) { const cacheAge = now.getTime() - cached.cachedAt.getTime(); const ticketExpired = cached.ticket.expiresAt < now; if (cacheAge > ttlMs || ticketExpired) { session.tickets.delete(principal); removedTickets++; } } if (session.tickets.size === 0) { this.sessions.delete(sessionId); removedSessions++; } } if (removedTickets > 0 || removedSessions > 0) { this.metrics.expired += removedTickets; } } /** * Find oldest ticket in session (for LRU eviction) * * @param session - Session cache * @returns Principal of oldest ticket */ findOldestTicket(session) { let oldestPrincipal; let oldestAccess = /* @__PURE__ */ new Date(); for (const [principal, cached] of session.tickets.entries()) { if (cached.lastAccess < oldestAccess) { oldestAccess = cached.lastAccess; oldestPrincipal = principal; } } return oldestPrincipal; } /** * Start periodic cleanup task */ startCleanup() { this.cleanupInterval = setInterval(() => { this.cleanup().catch((error) => { console.error("Ticket cache cleanup failed:", error); }); }, 60 * 1e3); if (this.cleanupInterval.unref) { this.cleanupInterval.unref(); } } /** * Stop periodic cleanup and clear all cache */ async destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = void 0; } this.sessions.clear(); this.metrics = { hits: 0, misses: 0, expired: 0 }; } }; // src/kerberos-module.ts var KerberosDelegationModule = class { /** * Module name */ name; /** * Module type */ type = "authentication"; config; client; ticketCache; tokenExchangeService; // Unused - token exchange handled by AuthenticationService tokenExchangeConfig; // Unused - maintained for API consistency /** * Create a new Kerberos delegation module * * @param name - Module name (e.g., 'kerberos', 'kerberos1', 'kerberos2') * Defaults to 'kerberos' for backward compatibility */ constructor(name = "kerberos") { this.name = name; } /** * Initialize Kerberos delegation module * * @param config - Kerberos configuration * @throws {Error} If initialization fails */ async initialize(config) { console.log(` [KERBEROS-MODULE:${this.name}] Initializing Kerberos delegation module`); console.log(`[KERBEROS-MODULE:${this.name}] Configuration:`, { domainController: config.domainController, realm: config.realm, servicePrincipalName: config.servicePrincipalName, kdc: config.kdc, enableS4U2Self: config.enableS4U2Self, enableS4U2Proxy: config.enableS4U2Proxy, allowedDelegationTargetsCount: config.allowedDelegationTargets?.length || 0, ticketCacheEnabled: config.ticketCache?.enabled !== false }); this.config = config; console.log(`[KERBEROS-MODULE:${this.name}] Creating Kerberos client`); this.client = new KerberosClient(config); if (config.ticketCache?.enabled !== false) { console.log(`[KERBEROS-MODULE:${this.name}] Initializing ticket cache:`, { ttlSeconds: config.ticketCache?.ttlSeconds ?? 3600, renewThresholdSeconds: config.ticketCache?.renewThresholdSeconds ?? 300 }); this.ticketCache = new TicketCache({ enabled: true, ttlSeconds: config.ticketCache?.ttlSeconds ?? 3600, renewThresholdSeconds: config.ticketCache?.renewThresholdSeconds ?? 300, maxEntriesPerSession: 10, sessionTimeoutMs: 15 * 60 * 1e3 }); } try { console.log(`[KERBEROS-MODULE:${this.name}] Obtaining service ticket (TGT)`); await this.client.obtainServiceTicket(); console.log(`[KERBEROS-MODULE:${this.name}] \u2713 Service ticket obtained successfully`); } catch (error) { console.error(`[KERBEROS-MODULE:${this.name}] \u2717 Failed to obtain service ticket:`, error); throw new Error( `Failed to initialize Kerberos module: ${error instanceof Error ? error.message : "Unknown error"}` ); } } /** * Perform Kerberos delegation * * NEW ARCHITECTURE (Phase 3): Token exchange happens BEFORE this method is called! * - AuthenticationService performs token exchange during authentication * - TE-JWT claims (including legacy_name) are validated and stored in UserSession * - This method uses pre-validated claims from session.legacyUsername * - No token exchange happens here anymore * * **Phase 2 Enhancement:** Now accepts optional context parameter with CoreContext. * This enables access to framework services if needed in the future. * * @param session - User session (with pre-validated TE-JWT claims) * @param action - Delegation action (s4u2self, s4u2proxy) * @param params - Action parameters * @param context - Optional context with sessionId and coreContext * @returns Delegation result with ticket */ async delegate(session, action, params, context) { console.log(` [KERBEROS-MODULE:${this.name}] delegate() called`); console.log(`[KERBEROS-MODULE:${this.name}] Action:`, action); console.log(`[KERBEROS-MODULE:${this.name}] Session:`, { userId: session.userId, legacyUsername: session.legacyUsername, sessionId: session.sessionId, hasDelegationToken: !!session.delegationToken, hasCustomClaims: !!session.customClaims }); console.log(`[KERBEROS-MODULE:${this.name}] Params:`, params); if (!session.legacyUsername) { console.error(`[KERBEROS-MODULE:${this.name}] Missing legacy_username claim in session`); console.error(`[KERBEROS-MODULE:${this.name}] This should have been validated during authentication!`); const auditEntry = { timestamp: /* @__PURE__ */ new Date(), userId: session.userId, action: `${this.name}:${action}`, success: false, reason: "User session missing legacy_username claim (authentication validation failed)", metadata: { params }, source: `delegation:${this.name}` }; return { success: false, error: "User session missing legacy_username claim for Kerberos delegation", auditTrail: auditEntry }; } const effectiveLegacyUsername = session.legacyUsername; console.log("[KERBEROS-MODULE] Using pre-validated legacy_username from session:", effectiveLegacyUsername); const userPrincipal = `${effectiveLegacyUsername}@${this.config.realm}`; console.log("[KERBEROS-MODULE] User principal:", userPrincipal); try { let result; console.log("[KERBEROS-MODULE] Executing action:", action); switch (action) { case "s4u2self": result = await this.performS4U2Self(session, userPrincipal); break; case "s4u2proxy": result = await this.performS4U2Proxy(session, userPrincipal, params); break; case "obtain-ticket": result = await this.performS4U2Self(session, userPrincipal); break; default: const auditEntry2 = { timestamp: /* @__PURE__ */ new Date(), userId: session.userId, action: `${this.name}:${action}`, success: false, reason: `Unsupported action: ${action}`, metadata: { params }, source: `delegation:${this.name}` }; return { success: false, error: `Unsupported Kerberos action: ${action}. Supported: s4u2self, s4u2proxy`, auditTrail: auditEntry2 }; } console.log("[KERBEROS-MODULE] Action completed successfully:", { hasResult: !!result, cached: result?.cached, hasTicket: !!result?.ticket }); const auditEntry = { timestamp: /* @__PURE__ */ new Date(), userId: session.userId, action: `${this.name}:${action}`, success: true, metadata: { userPrincipal, targetSPN: params.targetSPN, cached: result.cached }, source: `delegation:${this.name}` }; console.log("[KERBEROS-MODULE] \u2713 Delegation successful"); return { success: true, data: result, auditTrail: auditEntry }; } catch (error) { console.error("[KERBEROS-MODULE] \u2717 Delegation failed:", error); console.error("[KERBEROS-MODULE] Error details:", { message: error instanceof Error ? error.message : "Unknown error", stack: error instanceof Error ? error.stack : void 0 }); const auditEntry = { timestamp: /* @__PURE__ */ new Date(), userId: session.userId, action: `${this.name}:${action}`, success: false, error: error instanceof Error ? error.message : "Unknown error", metadata: { userPrincipal, params }, source: `delegation:${this.name}` }; return { success: false, error: error instanceof Error ? error.message : "Kerberos delegation failed", auditTrail: auditEntry }; } } /** * Perform S4U2Self delegation * * @param session - User session * @param userPrincipal - User principal name * @returns Ticket result */ async performS4U2Self(session, userPrincipal) { let ticket = await this.ticketCache?.get(session.sessionId, userPrincipal); let cached = false; if (!ticket) { ticket = await this.client.performS4U2Self(userPrincipal); if (this.ticketCache) { await this.ticketCache.set(session.sessionId, userPrincipal, ticket); } } else { cached = true; const needsRenewal = await this.ticketCache?.needsRenewal( session.sessionId, userPrincipal ); if (needsRenewal) { this.client.performS4U2Self(userPrincipal).then((newTicket) => { this.ticketCache?.set(session.sessionId, userPrincipal, newTicket); }).catch((error) => { console.error("Background ticket renewal failed:", error); }); } } await this.ticketCache?.heartbeat(session.sessionId); return { ticket, cached }; } /** * Perform S4U2Proxy delegation * * @param session - User session * @param userPrincipal - User principal name * @param params - Delegation parameters * @returns Proxy ticket result */ async performS4U2Proxy(session, userPrincipal, params) { if (!params.targetSPN) { throw new Error("targetSPN required for s4u2proxy action"); } const s4u2selfResult = await this.performS4U2Self(session, userPrincipal); const userTicket = s4u2selfResult.ticket; const proxyTicket = await this.client.performS4U2Proxy( userTicket, params.targetSPN ); return { ticket: proxyTicket, userTicket, cached: s4u2selfResult.cached }; } /** * Validate user session has required Kerberos attributes * * @param session - User session to validate * @returns true if user has legacy_username claim */ async validateAccess(session) { return !!session.legacyUsername; } /** * Check Kerberos module health * * @returns true if KDC is reachable and service ticket valid */ async healthCheck() { try { if (!this.client) { return false; } return await this.client.healthCheck(); } catch { return false; } } /** * Set token exchange service (API consistency with SQL module) * * NOTE: Kerberos module doesn't use TokenExchangeService directly. * Token exchange happens at authentication time (AuthenticationService), * and legacy_name claim is pre-validated in UserSession. * * This method is provided for API consistency but is a no-op. * * @param service - TokenExchangeService instance (unused) * @param config - Token exchange configuration (unused) */ setTokenExchangeService(service, config) { console.log("[KERBEROS-MODULE] setTokenExchangeService() called (no-op - token exchange handled by AuthenticationService)"); this.tokenExchangeService = service; this.tokenExchangeConfig = config; } /** * Destroy Kerberos module resources */ async destroy() { await this.ticketCache?.destroy(); await this.client?.destroy(); this.config = void 0; this.client = void 0; this.ticketCache = void 0; } /** * Get cache metrics (for monitoring) * * @returns Ticket cache metrics or undefined if cache disabled */ getCacheMetrics() { return this.ticketCache?.getMetrics(); } }; export { KerberosDelegationModule }; //# sourceMappingURL=index.js.map