@fastmcp-oauth/kerberos-delegation
Version:
Kerberos delegation module for FastMCP OAuth framework - Windows Kerberos Constrained Delegation support
851 lines (849 loc) • 29.5 kB
JavaScript
// 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