codecrucible-synth
Version:
Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability
401 lines (349 loc) • 11.1 kB
text/typescript
/**
* Enterprise JWT Authentication System
* Implements secure JWT token management with refresh tokens and session tracking
*/
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import { logger } from '../logger.js';
import {
AuthConfig,
User,
TokenPayload,
RefreshTokenPayload,
AuthResult,
AuthSession,
} from './auth-types.js';
export class JWTAuthenticator {
private config: AuthConfig;
private activeSessions = new Map<string, AuthSession>();
private refreshTokens = new Map<string, RefreshTokenPayload>();
private blacklistedTokens = new Set<string>();
constructor(config: AuthConfig) {
this.config = {
...config,
algorithms: config.algorithms || ['HS256'],
clockTolerance: config.clockTolerance || 30,
};
// Start cleanup interval for expired sessions
setInterval(() => this.cleanupExpiredSessions(), 60000); // Every minute
// TODO: Store interval ID and call clearInterval in cleanup
}
/**
* Generate access and refresh tokens
*/
async generateTokens(user: User, ipAddress: string, userAgent: string): Promise<AuthResult> {
try {
const sessionId = crypto.randomUUID();
const tokenFamily = crypto.randomUUID();
const now = Math.floor(Date.now() / 1000);
// Create session
const session: AuthSession = {
id: sessionId,
userId: user.id,
createdAt: new Date(),
lastAccessedAt: new Date(),
ipAddress,
userAgent,
isActive: true,
expiresAt: new Date(Date.now() + this.config.expiry * 1000),
};
this.activeSessions.set(sessionId, session);
// Create access token payload
const accessPayload: TokenPayload = {
sub: user.id,
iat: now,
exp: now + this.config.expiry,
iss: this.config.issuer,
aud: this.config.audience,
userId: user.id,
username: user.username,
roles: user.roles,
permissions: user.permissions,
sessionId,
};
// Generate access token
const accessToken = jwt.sign(accessPayload, this.config.secret, {
algorithm: this.config.algorithms[0] as jwt.Algorithm,
expiresIn: this.config.expiry,
});
let refreshToken: string | undefined;
if (this.config.refreshTokens) {
// Create refresh token payload
const refreshPayload: RefreshTokenPayload = {
userId: user.id,
sessionId,
tokenFamily,
exp: now + this.config.expiry * 7, // Refresh tokens last 7x longer
};
refreshToken = jwt.sign(refreshPayload, this.config.secret + '_refresh', {
algorithm: this.config.algorithms[0] as jwt.Algorithm,
expiresIn: this.config.expiry * 7,
});
this.refreshTokens.set(tokenFamily, refreshPayload);
}
logger.info('User authenticated successfully', {
userId: user.id,
sessionId,
ipAddress,
userAgent: userAgent.substring(0, 100),
});
return {
success: true,
user,
accessToken,
refreshToken,
expiresIn: this.config.expiry,
};
} catch (error) {
logger.error('Token generation failed', error as Error, {
userId: user.id,
ipAddress,
});
throw new AuthenticationError('Token generation failed', {
code: 'TOKEN_GENERATION_ERROR',
statusCode: 500,
});
}
}
/**
* Verify and decode access token
*/
async verifyToken(token: string): Promise<TokenPayload> {
try {
// Check if token is blacklisted
if (this.blacklistedTokens.has(token)) {
throw new AuthenticationError('Token has been revoked', {
code: 'TOKEN_REVOKED',
statusCode: 401,
});
}
const payload = jwt.verify(token, this.config.secret, {
algorithms: this.config.algorithms as jwt.Algorithm[],
issuer: this.config.issuer,
audience: this.config.audience,
clockTolerance: this.config.clockTolerance,
}) as TokenPayload;
// Verify session is still active
const session = this.activeSessions.get(payload.sessionId);
if (!session || !session.isActive || session.expiresAt < new Date()) {
throw new AuthenticationError('Session expired or invalid', {
code: 'SESSION_INVALID',
statusCode: 401,
});
}
// Update last accessed time
session.lastAccessedAt = new Date();
return payload;
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
throw new AuthenticationError('Invalid token', {
code: 'TOKEN_INVALID',
statusCode: 401,
});
}
if (error instanceof jwt.TokenExpiredError) {
throw new AuthenticationError('Token expired', {
code: 'TOKEN_EXPIRED',
statusCode: 401,
});
}
throw error;
}
}
/**
* Refresh access token using refresh token
*/
async refreshAccessToken(refreshToken: string): Promise<AuthResult> {
try {
const payload = jwt.verify(
refreshToken,
this.config.secret + '_refresh'
) as RefreshTokenPayload;
// Verify refresh token exists and is valid
const storedPayload = this.refreshTokens.get(payload.tokenFamily);
if (!storedPayload || storedPayload.exp < Math.floor(Date.now() / 1000)) {
throw new AuthenticationError('Refresh token expired or invalid', {
code: 'REFRESH_TOKEN_INVALID',
statusCode: 401,
});
}
// Get session
const session = this.activeSessions.get(payload.sessionId);
if (!session || !session.isActive) {
throw new AuthenticationError('Session invalid', {
code: 'SESSION_INVALID',
statusCode: 401,
});
}
// For security, we need to get the user again (in real app, from database)
// For now, create a mock user with basic info
const user: User = {
id: payload.userId,
username: 'user', // In production, fetch from database
email: 'user@example.com',
roles: ['user'],
permissions: ['read:own'],
metadata: {},
createdAt: new Date(),
isActive: true,
};
// Generate new tokens
return await this.generateTokens(user, session.ipAddress, session.userAgent);
} catch (error) {
logger.error('Token refresh failed', error as Error);
throw error;
}
}
/**
* Revoke token (add to blacklist)
*/
async revokeToken(token: string): Promise<void> {
try {
const payload = jwt.decode(token) as TokenPayload;
if (payload && payload.sessionId) {
// Deactivate session
const session = this.activeSessions.get(payload.sessionId);
if (session) {
session.isActive = false;
}
}
// Add to blacklist
this.blacklistedTokens.add(token);
logger.info('Token revoked', {
userId: payload?.userId,
sessionId: payload?.sessionId,
});
} catch (error) {
logger.error('Token revocation failed', error as Error);
throw error;
}
}
/**
* Revoke all tokens for a user
*/
async revokeAllUserTokens(userId: string): Promise<void> {
// Deactivate all user sessions
for (const [_sessionId, session] of this.activeSessions.entries()) {
if (session.userId === userId) {
session.isActive = false;
}
}
// Remove all refresh tokens for user
for (const [tokenFamily, payload] of this.refreshTokens.entries()) {
if (payload.userId === userId) {
this.refreshTokens.delete(tokenFamily);
}
}
logger.info('All tokens revoked for user', { userId });
}
/**
* Get active sessions for user
*/
getUserSessions(userId: string): AuthSession[] {
return Array.from(this.activeSessions.values()).filter(
session => session.userId === userId && session.isActive
);
}
/**
* Clean up expired sessions and tokens
*/
private cleanupExpiredSessions(): void {
const now = new Date();
let cleanedSessions = 0;
let cleanedRefreshTokens = 0;
// Clean expired sessions
for (const [sessionId, session] of this.activeSessions.entries()) {
if (session.expiresAt < now) {
this.activeSessions.delete(sessionId);
cleanedSessions++;
}
}
// Clean expired refresh tokens
const nowSeconds = Math.floor(now.getTime() / 1000);
for (const [tokenFamily, payload] of this.refreshTokens.entries()) {
if (payload.exp < nowSeconds) {
this.refreshTokens.delete(tokenFamily);
cleanedRefreshTokens++;
}
}
if (cleanedSessions > 0 || cleanedRefreshTokens > 0) {
logger.debug('Cleaned up expired auth artifacts', {
cleanedSessions,
cleanedRefreshTokens,
});
}
}
/**
* Express middleware for authentication
*/
middleware() {
return async (req: any, res: any, next: any) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({
error: 'Authentication required',
code: 'NO_AUTH_HEADER',
});
}
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
return res.status(401).json({
error: 'Invalid authorization header format',
code: 'INVALID_AUTH_FORMAT',
});
}
const token = parts[1];
const payload = await this.verifyToken(token);
// Attach user info to request
req.user = payload;
req.sessionId = payload.sessionId;
next();
} catch (error) {
if (error instanceof AuthenticationError) {
return res.status(error.statusCode).json({
error: error.message,
code: error.code,
});
}
logger.error('Authentication middleware error', error as Error);
return res.status(500).json({
error: 'Internal authentication error',
code: 'AUTH_INTERNAL_ERROR',
});
}
};
}
/**
* Hash password securely
*/
static async hashPassword(password: string): Promise<string> {
const saltRounds = 12;
return bcrypt.hash(password, saltRounds);
}
/**
* Verify password against hash
*/
static async verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
}
/**
* Custom Authentication Error class
*/
class AuthenticationError extends Error {
public code: string;
public statusCode: number;
public details?: Record<string, any>;
constructor(
message: string,
options: { code: string; statusCode: number; details?: Record<string, any> }
) {
super(message);
this.name = 'AuthenticationError';
this.code = options.code;
this.statusCode = options.statusCode;
this.details = options.details;
}
}