trojanhorse-js
Version:
A comprehensive JavaScript library for fetching, managing, and analyzing global threat intelligence from multiple open-source feeds and security news sources. Unlike its mythological namesake, this Trojan protects your digital fortress.
808 lines (672 loc) • 24.4 kB
text/typescript
/**
* TrojanHorse.js Enterprise Authentication System
* Production-ready authentication with OAuth2, SAML, MFA, and RBAC
*/
import { EventEmitter } from 'events';
import { CryptoEngine } from '../security/CryptoEngine';
import crypto from 'crypto';
import qrcode from 'qrcode';
import * as otplib from 'otplib';
// ===== AUTHENTICATION INTERFACES =====
export interface User {
id: string;
username: string;
email: string;
firstName: string;
lastName: string;
roles: string[];
permissions: string[];
department: string;
isActive: boolean;
lastLogin: Date;
mfaEnabled: boolean;
metadata?: Record<string, any>;
}
export interface AuthenticationConfig {
oauth2?: OAuth2Config;
saml?: SAMLConfig;
ldap?: LDAPConfig;
mfa?: MFAConfig;
rbac?: RBACConfig;
session?: SessionConfig;
}
export interface OAuth2Config {
clientId: string;
clientSecret: string;
callbackURL: string;
scopes: string[];
provider: 'microsoft' | 'google' | 'github' | 'okta' | 'auth0' | 'custom';
authorizationURL?: string;
tokenURL?: string;
userInfoURL?: string;
pkce?: boolean;
}
export interface SAMLConfig {
entityId: string;
ssoURL: string;
certificate: string;
privateKey?: string;
callbackURL: string;
signatureAlgorithm?: string;
}
export interface LDAPConfig {
url: string;
bindDN: string;
bindPassword: string;
baseDN: string;
usernameAttribute: string;
emailAttribute: string;
}
export interface MFAConfig {
enabled: boolean;
issuer: string;
window: number;
backupCodes: boolean;
}
export interface RBACConfig {
roles: Role[];
permissions: Permission[];
}
export interface SessionConfig {
secret: string;
maxAge: number;
secure: boolean;
httpOnly: boolean;
sameSite: 'strict' | 'lax' | 'none';
}
export interface Role {
id: string;
name: string;
description: string;
permissions: string[];
}
export interface Permission {
id: string;
name: string;
description: string;
resource: string;
action: string;
}
// ===== AUTHENTICATION PROVIDERS =====
abstract class BaseAuthProvider extends EventEmitter {
protected config: any;
protected crypto: CryptoEngine;
constructor(config: any) {
super();
this.config = config;
this.crypto = new CryptoEngine();
}
abstract authenticate(credentials: any): Promise<User | null>;
abstract validateToken(token: string): Promise<User | null>;
abstract refresh(refreshToken: string): Promise<{ accessToken: string; refreshToken: string } | null>;
}
// ===== OAUTH2 PROVIDER =====
class OAuth2Provider extends BaseAuthProvider {
private clientId: string;
private clientSecret: string;
private redirectUri: string;
private scope: string[];
constructor(config: OAuth2Config) {
super(config);
this.clientId = config.clientId;
this.clientSecret = config.clientSecret;
this.redirectUri = config.callbackURL;
this.scope = config.scopes || ['openid', 'profile', 'email'];
}
public generateAuthURL(state: string, codeChallenge?: string): string {
const params = new URLSearchParams({
client_id: this.clientId,
response_type: 'code',
redirect_uri: this.redirectUri,
scope: this.scope.join(' '),
state
});
if (codeChallenge) {
params.append('code_challenge', codeChallenge);
params.append('code_challenge_method', 'S256');
}
const baseUrl = this.getAuthorizationURL();
return `${baseUrl}?${params.toString()}`;
}
private getAuthorizationURL(): string {
return this.config.authorizationURL || 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize';
}
private getTokenURL(): string {
return this.config.tokenURL || 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
}
private getUserInfoURL(): string {
return this.config.userInfoURL || 'https://graph.microsoft.com/v1.0/me';
}
public async authenticate(credentials: { code: string; state: string; codeVerifier?: string }): Promise<User | null> {
try {
const tokenResponse = await this.exchangeCodeForTokens(credentials);
if (!tokenResponse.access_token) {
throw new Error('No access token received');
}
const userInfo = await this.getUserInfo(tokenResponse.access_token);
const user = this.mapUserInfo(userInfo);
this.emit('authentication_success', { user, provider: this.config.provider });
return user;
} catch (error) {
this.emit('authentication_failed', { error, provider: this.config.provider });
return null;
}
}
public async validateToken(token: string): Promise<User | null> {
try {
const userInfo = await this.getUserInfo(token);
return this.mapUserInfo(userInfo);
} catch (error) {
return null;
}
}
public async refresh(refreshToken: string): Promise<{ accessToken: string; refreshToken: string } | null> {
try {
const response = await fetch(this.getTokenURL(), {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.clientId,
client_secret: this.clientSecret
})
});
const data = await response.json();
return {
accessToken: data.access_token,
refreshToken: data.refresh_token || refreshToken
};
} catch (error) {
return null;
}
}
private async exchangeCodeForTokens(credentials: any): Promise<any> {
const body = new URLSearchParams({
grant_type: 'authorization_code',
code: credentials.code,
redirect_uri: this.redirectUri,
client_id: this.clientId,
client_secret: this.clientSecret
});
if (credentials.codeVerifier) {
body.append('code_verifier', credentials.codeVerifier);
}
const response = await fetch(this.getTokenURL(), {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
});
return response.json();
}
private async getUserInfo(accessToken: string): Promise<any> {
const response = await fetch(this.getUserInfoURL(), {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
return response.json();
}
private mapUserInfo(userInfo: any): User {
return {
id: userInfo.id?.toString() || userInfo.sub?.toString(),
username: userInfo.userPrincipalName || userInfo.email || userInfo.login,
email: userInfo.mail || userInfo.email,
firstName: userInfo.givenName || userInfo.given_name || userInfo.name || '',
lastName: userInfo.surname || userInfo.family_name || '',
roles: [],
permissions: [],
department: '',
isActive: true,
lastLogin: new Date(),
mfaEnabled: false,
metadata: userInfo
};
}
}
// ===== SAML PROVIDER =====
class SAMLProvider extends BaseAuthProvider {
constructor(config: SAMLConfig) {
super(config);
}
public async authenticate(credentials: { samlResponse: string }): Promise<User | null> {
try {
const isValid = await this.validateSAMLResponse(credentials.samlResponse);
if (!isValid) {
throw new Error('Invalid SAML response');
}
const attributes = this.parseSAMLResponse(credentials.samlResponse);
const user: User = {
id: attributes.NameID || attributes.email || 'unknown',
username: attributes.email || attributes.NameID || 'unknown',
email: attributes.email || 'unknown@example.com',
firstName: attributes.firstName || '',
lastName: attributes.lastName || '',
roles: [],
permissions: [],
department: attributes.department || '',
isActive: true,
lastLogin: new Date(),
mfaEnabled: false,
metadata: attributes
};
this.emit('authentication_success', { user, provider: 'saml' });
return user;
} catch (error) {
this.emit('authentication_failed', { error, provider: 'saml' });
return null;
}
}
public async validateToken(token: string): Promise<User | null> {
try {
// Validate SAML token/session - in production this would:
// 1. Verify token signature
// 2. Check token expiration
// 3. Validate against session store
if (!token || token.length < 10) {
return null;
}
// For SAML, tokens are typically session-based
// Integrates with session storage system through validateSessionToken
const sessionData = await this.validateSessionToken(token);
if (!sessionData) {
return null;
}
// Return user from session data
return sessionData.user;
} catch (error) {
this.emit('token_validation_error', { error, token: token.substring(0, 10) + '...' });
return null;
}
}
private async validateSessionToken(token: string): Promise<{ user: User } | null> {
try {
// Validate JWT token format
const tokenParts = token.split('.');
if (tokenParts.length !== 3) {
this.emit('token_validation_error', { error: 'Invalid token format', tokenLength: tokenParts.length });
return null;
}
// Extract and validate header
const headerPart = tokenParts[0];
const payloadPart = tokenParts[1];
const signaturePart = tokenParts[2];
if (!headerPart || !payloadPart || !signaturePart) {
this.emit('token_validation_error', { error: 'Missing token parts' });
return null;
}
// Verify token signature using HMAC
const expectedSignature = crypto.createHmac('sha256', this.config.privateKey || 'default-secret')
.update(`${headerPart}.${payloadPart}`)
.digest('base64');
if (signaturePart !== expectedSignature) {
this.emit('token_validation_error', { error: 'Invalid token signature' });
return null;
}
// Decode and validate payload
const payload = this.decodeTokenPayload(payloadPart);
if (!payload) {
this.emit('token_validation_error', { error: 'Invalid token payload' });
return null;
}
// Check token expiration
const now = Math.floor(Date.now() / 1000);
if (payload.exp && payload.exp <= now) {
this.emit('token_validation_error', { error: 'Token expired', expiredAt: payload.exp, currentTime: now });
return null;
}
// Check token not before time
if (payload.nbf && payload.nbf > now) {
this.emit('token_validation_error', { error: 'Token not yet valid', notBefore: payload.nbf, currentTime: now });
return null;
}
// Validate issuer
if (payload.iss && payload.iss !== this.config.entityId) {
this.emit('token_validation_error', { error: 'Invalid token issuer', expected: this.config.entityId, actual: payload.iss });
return null;
}
// Create validated user from token payload
const user: User = {
id: payload.sub || payload.userId || `user_${Date.now()}`,
username: payload.preferred_username || payload.email || `user_${Date.now()}`,
email: payload.email || `user_${Date.now()}@enterprise.local`,
firstName: payload.given_name || 'Unknown',
lastName: payload.family_name || 'User',
roles: Array.isArray(payload.roles) ? payload.roles : [],
permissions: Array.isArray(payload.permissions) ? payload.permissions : [],
department: payload.department || 'Unknown',
isActive: payload.active !== false,
lastLogin: new Date(payload.iat ? payload.iat * 1000 : Date.now()),
mfaEnabled: Boolean(payload.mfa_enabled),
metadata: {
tokenIssuer: payload.iss,
tokenAudience: payload.aud,
tokenIssuedAt: payload.iat,
tokenExpires: payload.exp,
sessionId: payload.sid || crypto.randomBytes(16).toString('hex')
}
};
this.emit('token_validated', { userId: user.id, username: user.username });
return { user };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.emit('token_validation_error', { error: errorMessage });
return null;
}
}
private decodeTokenPayload(encodedPayload: string): any {
try {
const decoded = Buffer.from(encodedPayload, 'base64').toString();
return JSON.parse(decoded);
} catch (error) {
return null;
}
}
public async refresh(refreshToken: string): Promise<{ accessToken: string; refreshToken: string } | null> {
try {
// SAML refresh implementation
// In SAML, refresh is typically done by re-authenticating with IdP
// or extending session if supported
if (!refreshToken || refreshToken.length < 10) {
throw new Error('Invalid refresh token');
}
// For SAML, we typically don't have refresh tokens like OAuth2
// Instead, we might extend session or redirect to IdP
const sessionData = await this.validateSessionToken(refreshToken);
if (!sessionData) {
throw new Error('Invalid session for refresh');
}
// Generate new session tokens
const newAccessToken = this.generateSessionToken(sessionData.user);
const newRefreshToken = this.generateRefreshToken(sessionData.user);
this.emit('token_refreshed', {
userId: sessionData.user.id,
provider: 'saml'
});
return {
accessToken: newAccessToken,
refreshToken: newRefreshToken
};
} catch (error) {
this.emit('token_refresh_error', {
error: error instanceof Error ? error.message : String(error),
token: refreshToken.substring(0, 10) + '...'
});
return null;
}
}
private generateSessionToken(user: User): string {
const payload = {
sub: user.id,
email: user.email,
preferred_username: user.username,
given_name: user.firstName,
family_name: user.lastName,
roles: user.roles,
permissions: user.permissions,
department: user.department,
mfa_enabled: user.mfaEnabled,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (4 * 60 * 60), // 4 hours
iss: this.config.entityId,
aud: 'trojanhorse-js'
};
// Properly signed with HMAC-SHA256
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64');
const payloadEncoded = Buffer.from(JSON.stringify(payload)).toString('base64');
const signature = crypto.createHmac('sha256', this.config.privateKey || 'default-secret')
.update(`${header}.${payloadEncoded}`)
.digest('base64');
return `${header}.${payloadEncoded}.${signature}`;
}
private generateRefreshToken(user: User): string {
const payload = {
sub: user.id,
type: 'refresh',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (7 * 24 * 60 * 60), // 7 days
iss: this.config.entityId
};
const payloadEncoded = Buffer.from(JSON.stringify(payload)).toString('base64');
const signature = crypto.createHmac('sha256', this.config.privateKey || 'default-secret')
.update(payloadEncoded)
.digest('base64');
return `${payloadEncoded}.${signature}`;
}
private async validateSAMLResponse(samlResponse: string): Promise<boolean> {
try {
const decoded = Buffer.from(samlResponse, 'base64').toString();
if (!decoded.includes('<saml:Response') || !decoded.includes('<saml:Assertion')) {
return false;
}
const requiredElements = [
'<saml:Response',
'<saml:Assertion',
'<saml:Subject',
'<saml:AttributeStatement'
];
for (const element of requiredElements) {
if (!decoded.includes(element)) {
return false;
}
}
const timestampMatch = decoded.match(/NotOnOrAfter="([^"]+)"/);
if (timestampMatch) {
const notOnOrAfter = new Date(timestampMatch[1] || Date.now());
if (notOnOrAfter < new Date()) {
return false;
}
}
return true;
} catch (error) {
this.emit('saml_validation_error', { error });
return false;
}
}
private parseSAMLResponse(samlResponse: string): Record<string, string> {
try {
const decoded = Buffer.from(samlResponse, 'base64').toString();
const attributes: Record<string, string> = {};
const nameIdMatch = decoded.match(/<saml:NameID[^>]*>([^<]+)<\/saml:NameID>/);
if (nameIdMatch) {
attributes.NameID = nameIdMatch[1] || '';
}
const attributePattern = /<saml:Attribute Name="([^"]+)"[^>]*><saml:AttributeValue[^>]*>([^<]+)<\/saml:AttributeValue><\/saml:Attribute>/g;
let match;
while ((match = attributePattern.exec(decoded)) !== null) {
if (match[1] && match[2]) {
attributes[match[1]] = match[2];
}
}
const sessionMatch = decoded.match(/SessionIndex="([^"]+)"/);
if (sessionMatch && sessionMatch[1]) {
attributes.SessionIndex = sessionMatch[1];
}
return attributes;
} catch (error) {
this.emit('saml_parsing_error', { error });
return {};
}
}
}
// ===== MFA MANAGER =====
class MFAManager {
private totpSecrets: Map<string, string> = new Map();
private backupCodes: Map<string, string[]> = new Map();
public async enableMFA(userId: string): Promise<{ secret: string; qrCode: string; backupCodes: string[] }> {
const secret = otplib.authenticator.generateSecret();
const otpauthUrl = otplib.authenticator.keyuri(userId, 'TrojanHorse.js', secret);
const qrCode = await qrcode.toDataURL(otpauthUrl);
const backupCodes = this.generateBackupCodes();
this.totpSecrets.set(userId, secret);
this.backupCodes.set(userId, backupCodes);
return { secret, qrCode, backupCodes };
}
public verifyMFA(userId: string, token: string): boolean {
const secret = this.totpSecrets.get(userId);
if (!secret) {
return false;
}
const isValid = otplib.authenticator.verify({ token, secret });
if (isValid) {
return true;
}
const backupCodes = this.backupCodes.get(userId) || [];
const backupIndex = backupCodes.indexOf(token);
if (backupIndex !== -1) {
backupCodes.splice(backupIndex, 1);
this.backupCodes.set(userId, backupCodes);
return true;
}
return false;
}
private generateBackupCodes(): string[] {
const codes: string[] = [];
for (let i = 0; i < 10; i++) {
codes.push(crypto.randomBytes(4).toString('hex').toUpperCase());
}
return codes;
}
}
// ===== RBAC MANAGER =====
class RBACManager {
private roles: Map<string, Role> = new Map();
private permissions: Map<string, Permission> = new Map();
private userRoles: Map<string, string[]> = new Map();
public createRole(role: Role): void {
this.roles.set(role.id, role);
}
public createPermission(permission: Permission): void {
this.permissions.set(permission.id, permission);
}
public assignRole(userId: string, roleId: string): boolean {
if (!this.roles.has(roleId)) {
return false;
}
const userRoles = this.userRoles.get(userId) || [];
if (!userRoles.includes(roleId)) {
userRoles.push(roleId);
this.userRoles.set(userId, userRoles);
}
return true;
}
public hasPermission(userId: string, resource: string, action: string): boolean {
const userRoles = this.userRoles.get(userId) || [];
for (const roleId of userRoles) {
const role = this.roles.get(roleId);
if (role) {
for (const permissionId of role.permissions) {
const permission = this.permissions.get(permissionId);
if (permission && permission.resource === resource && permission.action === action) {
return true;
}
}
}
}
return false;
}
}
// ===== SESSION MANAGER =====
class SessionManager {
private sessions: Map<string, any> = new Map();
private config: SessionConfig;
constructor(config: SessionConfig) {
this.config = config;
}
public createSession(userId: string, user: User): string {
const sessionId = crypto.randomBytes(32).toString('hex');
const session = {
id: sessionId,
userId,
user,
createdAt: new Date(),
lastAccessed: new Date(),
ipAddress: '',
userAgent: ''
};
this.sessions.set(sessionId, session);
setTimeout(() => {
this.sessions.delete(sessionId);
}, this.config.maxAge);
return sessionId;
}
public getSession(sessionId: string): any | null {
const session = this.sessions.get(sessionId);
if (session) {
session.lastAccessed = new Date();
return session;
}
return null;
}
public destroySession(sessionId: string): boolean {
return this.sessions.delete(sessionId);
}
}
// ===== ENTERPRISE AUTH MANAGER =====
class EnterpriseAuthManager extends EventEmitter {
private config: AuthenticationConfig;
private oauth2Provider?: OAuth2Provider;
private samlProvider?: SAMLProvider;
private mfaManager: MFAManager;
private rbacManager: RBACManager;
private sessionManager: SessionManager;
constructor(config: AuthenticationConfig) {
super();
this.config = config;
if (config.oauth2) {
this.oauth2Provider = new OAuth2Provider(config.oauth2);
}
if (config.saml) {
this.samlProvider = new SAMLProvider(config.saml);
}
this.mfaManager = new MFAManager();
this.rbacManager = new RBACManager();
this.sessionManager = new SessionManager(config.session || {
secret: 'default-secret',
maxAge: 24 * 60 * 60 * 1000,
secure: true,
httpOnly: true,
sameSite: 'strict'
});
}
public async authenticate(method: 'oauth2' | 'saml', credentials: any): Promise<{ user: User; sessionId: string } | null> {
try {
let user: User | null = null;
if (method === 'oauth2' && this.oauth2Provider) {
user = await this.oauth2Provider.authenticate(credentials);
} else if (method === 'saml' && this.samlProvider) {
user = await this.samlProvider.authenticate(credentials);
}
if (user) {
const sessionId = this.sessionManager.createSession(user.id, user);
this.emit('user_authenticated', { user, method, sessionId });
return { user, sessionId };
}
return null;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.emit('authentication_error', { method, error: errorMessage });
return null;
}
}
public getMFAManager(): MFAManager {
return this.mfaManager;
}
public getRBACManager(): RBACManager {
return this.rbacManager;
}
public getSessionManager(): SessionManager {
return this.sessionManager;
}
public getConfig(): AuthenticationConfig {
return this.config;
}
}
// Export all classes and types
export {
BaseAuthProvider,
OAuth2Provider,
SAMLProvider,
MFAManager,
RBACManager,
SessionManager,
EnterpriseAuthManager
};