/**
* OAuth 2.1 Session Manager for Multi-tenant MCP Server
* Handles stateless session management with 24-hour expiration
*/
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
export interface Session {
sessionId: string;
username: string;
userKey: string;
accountKey: string;
divisionId: number;
token: string;
expiresAt: Date;
createdAt: Date;
realm?: string;
isMSP: boolean;
}
export interface OAuthTokenPayload {
sessionId: string;
username: string;
userKey: string;
accountKey: string;
divisionId: number;
realm?: string;
isMSP: boolean;
iat: number;
exp: number;
}
export class OAuthSessionManager {
private sessions: Map<string, Session> = new Map();
private readonly JWT_SECRET: string;
private readonly SESSION_DURATION_HOURS = 24;
constructor() {
// Use environment variable or generate a secure random secret
this.JWT_SECRET = process.env.OAUTH_JWT_SECRET || crypto.randomBytes(64).toString('hex');
// Clean up expired sessions every hour
setInterval(() => this.cleanupExpiredSessions(), 60 * 60 * 1000);
}
/**
* Create a new session after successful authentication
*/
createSession(
username: string,
userKey: string,
accountKey: string,
divisionId: number,
token: string,
realm?: string,
isMSP: boolean = false
): { sessionId: string; bearerToken: string; authUrl: string } {
const sessionId = this.generateSessionId();
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + this.SESSION_DURATION_HOURS);
const session: Session = {
sessionId,
username,
userKey,
accountKey,
divisionId,
token,
expiresAt,
createdAt: new Date(),
realm,
isMSP
};
this.sessions.set(sessionId, session);
// Create JWT bearer token
const bearerToken = this.createBearerToken(session);
// Generate OAuth callback URL
const authUrl = this.generateAuthUrl(sessionId, bearerToken);
console.error(`[OAUTH] Created session ${sessionId} for ${username}, expires at ${expiresAt.toISOString()}`);
return { sessionId, bearerToken, authUrl };
}
/**
* Create a JWT bearer token for the session
*/
private createBearerToken(session: Session): string {
const payload: OAuthTokenPayload = {
sessionId: session.sessionId,
username: session.username,
userKey: session.userKey,
accountKey: session.accountKey,
divisionId: session.divisionId,
realm: session.realm,
isMSP: session.isMSP,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(session.expiresAt.getTime() / 1000)
};
return jwt.sign(payload, this.JWT_SECRET, { algorithm: 'HS256' });
}
/**
* Validate bearer token and return session
*/
validateBearerToken(bearerToken: string): Session | null {
try {
// Remove 'Bearer ' prefix if present
const token = bearerToken.replace(/^Bearer\s+/i, '');
// Verify and decode JWT
const payload = jwt.verify(token, this.JWT_SECRET) as OAuthTokenPayload;
// Check if session exists and is not expired
const session = this.sessions.get(payload.sessionId);
if (!session) {
console.error(`[OAUTH] Session ${payload.sessionId} not found`);
return null;
}
if (new Date() > session.expiresAt) {
console.error(`[OAUTH] Session ${payload.sessionId} expired`);
this.sessions.delete(payload.sessionId);
return null;
}
return session;
} catch (error) {
console.error('[OAUTH] Invalid bearer token:', error);
return null;
}
}
/**
* Get session by ID
*/
getSession(sessionId: string): Session | null {
const session = this.sessions.get(sessionId);
if (!session) {
return null;
}
// Check if expired
if (new Date() > session.expiresAt) {
this.sessions.delete(sessionId);
return null;
}
return session;
}
/**
* Remove a session
*/
removeSession(sessionId: string): void {
this.sessions.delete(sessionId);
console.error(`[OAUTH] Removed session ${sessionId}`);
}
/**
* Generate a secure session ID
*/
private generateSessionId(): string {
return crypto.randomBytes(32).toString('hex');
}
/**
* Generate OAuth authentication URL
*/
private generateAuthUrl(sessionId: string, bearerToken: string): string {
const baseUrl = process.env.MCP_BASE_URL || 'https://mcp.umbrellacost.io';
const params = new URLSearchParams({
session_id: sessionId,
token: bearerToken,
redirect_uri: `${baseUrl}/oauth/callback`
});
return `${baseUrl}/oauth/authorize?${params.toString()}`;
}
/**
* Clean up expired sessions
*/
private cleanupExpiredSessions(): void {
const now = new Date();
let cleaned = 0;
for (const [sessionId, session] of this.sessions.entries()) {
if (now > session.expiresAt) {
this.sessions.delete(sessionId);
cleaned++;
}
}
if (cleaned > 0) {
console.error(`[OAUTH] Cleaned up ${cleaned} expired sessions`);
}
}
/**
* Get active session count
*/
getActiveSessionCount(): number {
return this.sessions.size;
}
/**
* Get all active sessions (for monitoring)
*/
getActiveSessions(): Array<{ sessionId: string; username: string; expiresAt: Date }> {
const activeSessions = [];
const now = new Date();
for (const [sessionId, session] of this.sessions.entries()) {
if (now <= session.expiresAt) {
activeSessions.push({
sessionId,
username: session.username,
expiresAt: session.expiresAt
});
}
}
return activeSessions;
}
}