okta-mcp-server
Version:
Model Context Protocol (MCP) server for Okta API operations with support for bulk operations and caching
347 lines • 12.7 kB
JavaScript
/**
* Audit middleware for automatic capture of MCP operations
*/
import { randomUUID } from 'crypto';
import { logger } from '../../utils/logger.js';
export class AuditMiddleware {
auditLogger;
extractActor;
extractContext;
skipPaths;
performanceThreshold;
activeRequests = new Map();
constructor(options) {
this.auditLogger = options.auditLogger;
this.extractActor = options.extractActor || this.defaultExtractActor;
this.extractContext = options.extractContext || this.defaultExtractContext;
this.skipPaths = new Set(options.skipPaths || []);
this.performanceThreshold = options.performanceThreshold || 5000; // 5 seconds
}
/**
* Wrap a tool handler with audit logging
*/
wrapToolHandler(toolName, handler) {
return (async (...args) => {
const request = args[0];
const requestId = randomUUID();
const startTime = Date.now();
const memoryStart = process.memoryUsage().heapUsed;
// Store request info for tracking
this.activeRequests.set(requestId, { startTime, memoryStart });
// Extract actor and context
const actor = this.extractActor(request);
const context = {
requestId,
...this.extractContext(request),
};
let result;
let error;
try {
// Call the actual handler
result = await handler(...args);
// Log successful tool call
await this.logToolCall({
timestamp: new Date(startTime),
actor,
action: {
type: 'tool.call',
method: toolName,
result: result.isError ? 'failure' : 'success',
},
request: {
method: toolName,
parameters: request.params.arguments,
},
response: {
status: result.isError ? 400 : 200,
},
performance: this.calculatePerformance(requestId),
context,
});
return result;
}
catch (err) {
error = err;
// Log error
await this.logToolCall({
timestamp: new Date(startTime),
actor,
action: {
type: 'tool.call',
method: toolName,
result: 'error',
errorCode: err.code || 'UNKNOWN',
errorMessage: err instanceof Error ? err.message : String(err),
},
request: {
method: toolName,
parameters: request.params.arguments,
},
response: {
status: 500,
error: String(err),
},
performance: this.calculatePerformance(requestId),
context,
});
throw err;
}
finally {
// Clean up request tracking
this.activeRequests.delete(requestId);
}
});
}
/**
* Wrap a resource handler with audit logging
*/
wrapResourceHandler(resourceType, handler) {
return (async (...args) => {
const uri = args[0];
const requestId = randomUUID();
const startTime = Date.now();
const memoryStart = process.memoryUsage().heapUsed;
// Store request info for tracking
this.activeRequests.set(requestId, { startTime, memoryStart });
// Extract resource info from URI
const resource = this.extractResourceFromUri(uri);
// Extract actor and context (from second arg if available)
const request = args[1];
const actor = this.extractActor(request);
const context = {
requestId,
...this.extractContext(request),
};
let result;
let error;
try {
// Call the actual handler
result = await handler(...args);
// Log successful resource read
await this.logResourceRead({
timestamp: new Date(startTime),
actor,
action: {
type: 'resource.read',
method: resourceType,
result: 'success',
},
resource,
request: {
method: 'read',
path: uri,
},
response: {
status: 200,
},
performance: this.calculatePerformance(requestId),
context,
});
return result;
}
catch (err) {
error = err;
// Log error
await this.logResourceRead({
timestamp: new Date(startTime),
actor,
action: {
type: 'resource.read',
method: resourceType,
result: 'error',
errorCode: err.code || 'UNKNOWN',
errorMessage: err instanceof Error ? err.message : String(err),
},
resource,
request: {
method: 'read',
path: uri,
},
response: {
status: err.status || 500,
error: String(err),
},
performance: this.calculatePerformance(requestId),
context,
});
throw err;
}
finally {
// Clean up request tracking
this.activeRequests.delete(requestId);
}
});
}
/**
* Create an Express-style middleware for HTTP transports
*/
createHttpMiddleware() {
return async (req, res, next) => {
const requestId = req.headers['x-request-id'] || randomUUID();
const startTime = Date.now();
const memoryStart = process.memoryUsage().heapUsed;
// Store request info
this.activeRequests.set(requestId, { startTime, memoryStart });
// Extract actor
const actor = {
id: req.user?.id || req.headers['x-client-id'] || 'anonymous',
type: req.user ? 'user' : 'service',
ipAddress: req.ip || req.connection?.remoteAddress,
userAgent: req.headers['user-agent'],
sessionId: req.session?.id,
};
// Extract context
const context = {
requestId,
correlationId: req.headers['x-correlation-id'],
clientApplication: req.headers['x-client-application'],
clientVersion: req.headers['x-client-version'],
};
// Capture response
const originalSend = res.send;
const middleware = this;
res.send = function (data) {
res.send = originalSend;
// Log the request
const performance = middleware.calculatePerformance(requestId);
middleware.auditLogger
.log({
timestamp: new Date(startTime),
actor,
action: {
type: 'http.request',
method: req.method,
result: res.statusCode < 400 ? 'success' : 'failure',
},
request: {
method: req.method,
path: req.path,
parameters: { ...req.query, ...req.body },
size: req.headers['content-length']
? parseInt(req.headers['content-length'])
: undefined,
},
response: {
status: res.statusCode,
size: Buffer.byteLength(data),
},
performance,
context,
})
.catch((err) => logger.error('Failed to log audit entry:', err));
// Clean up
middleware.activeRequests.delete(requestId);
return originalSend.call(this, data);
};
next();
};
}
/**
* Log a tool call
*/
async logToolCall(entry) {
// Check performance threshold
if (entry.performance && entry.performance.duration > this.performanceThreshold) {
logger.warn(`Tool call ${entry.action.method} exceeded performance threshold: ${entry.performance.duration}ms`);
}
await this.auditLogger.log(entry);
}
/**
* Log a resource read
*/
async logResourceRead(entry) {
// Check performance threshold
if (entry.performance && entry.performance.duration > this.performanceThreshold) {
logger.warn(`Resource read ${entry.action.method} exceeded performance threshold: ${entry.performance.duration}ms`);
}
await this.auditLogger.log(entry);
}
/**
* Default actor extraction
*/
defaultExtractActor(request) {
return {
id: request?.user?.id || request?.clientId || 'system',
type: request?.user ? 'user' : 'service',
ipAddress: request?.ip,
userAgent: request?.userAgent,
sessionId: request?.sessionId,
};
}
/**
* Default context extraction
*/
defaultExtractContext(request) {
return {
correlationId: request?.correlationId,
environment: process.env['NODE_ENV'],
clientApplication: request?.clientApplication,
clientVersion: request?.clientVersion,
};
}
/**
* Extract resource info from URI
*/
extractResourceFromUri(uri) {
const match = uri.match(/^okta:\/\/([^/]+)(?:\/(.+))?/);
if (!match)
return undefined;
return {
type: match[1],
id: match[2] || 'all',
};
}
/**
* Calculate performance metrics
*/
calculatePerformance(requestId) {
const requestInfo = this.activeRequests.get(requestId);
if (!requestInfo)
return undefined;
const duration = Date.now() - requestInfo.startTime;
const memoryUsed = process.memoryUsage().heapUsed - requestInfo.memoryStart;
return {
duration,
memoryUsed: memoryUsed > 0 ? memoryUsed : undefined,
};
}
/**
* Create audit context for a request
*/
static createContext(request) {
return {
requestId: request?.id || randomUUID(),
correlationId: request?.correlationId,
environment: process.env['NODE_ENV'],
region: process.env['AWS_REGION'] || process.env['REGION'],
clientApplication: request?.clientApplication,
clientVersion: request?.clientVersion,
};
}
/**
* Create audit actor from various sources
*/
static createActor(source) {
if (source?.user) {
return {
id: source.user.id || source.user.email || 'unknown',
type: 'user',
ipAddress: source.ip || source.ipAddress,
userAgent: source.userAgent || source.headers?.['user-agent'],
sessionId: source.sessionId || source.session?.id,
};
}
if (source?.service) {
return {
id: source.service.id || source.service.name || 'unknown-service',
type: 'service',
ipAddress: source.ip || source.ipAddress,
};
}
return {
id: 'system',
type: 'system',
};
}
}
//# sourceMappingURL=audit-middleware.js.map