Files
dbis_core/src/integration/api-gateway/middleware/auth.middleware.ts
2026-04-18 12:05:17 -07:00

267 lines
8.2 KiB
TypeScript

// Zero-Trust Authentication Middleware
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { sovereignIdentityFabric } from '@/sovereign/identity/sovereign-identity-fabric.service';
import { hsmService } from '@/integration/hsm/hsm.service';
import { DbisError, ErrorCode, JwtPayload } from '@/shared/types';
import { logger } from '@/infrastructure/monitoring/logger';
import { getEnv } from '@/shared/config/env-validator';
export interface AuthenticatedRequest extends Request {
sovereignBankId?: string;
employeeId?: string;
email?: string;
name?: string;
roleName?: string;
permissions?: string[];
sessionType?: 'portal' | 'service';
portalSurface?: 'admin' | 'member' | 'core';
identityType?: string;
apiRole?: string;
}
function isPortalSession(payload: JwtPayload): boolean {
return payload.sessionType === 'portal' || payload.identityType === 'WEB_PORTAL';
}
/**
* Extract Sovereign Identity Token (SIT) from Authorization header
*/
export function extractSitToken(req: Request): string | null {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('SOV-TOKEN ')) {
return null;
}
return authHeader.substring(10);
}
/**
* Verify request signature (X-SOV-SIGNATURE)
*/
export async function verifyRequestSignature(
req: Request,
sovereignBankId: string,
identityType: string
): Promise<boolean> {
const signature = req.headers['x-sov-signature'] as string;
const timestamp = req.headers['x-sov-timestamp'] as string;
const nonce = req.headers['x-sov-nonce'] as string;
if (!signature || !timestamp || !nonce) {
logger.warn('Missing request signature headers', {
sovereignBankId,
identityType,
hasSignature: !!signature,
hasTimestamp: !!timestamp,
hasNonce: !!nonce,
});
return false;
}
// Validate timestamp (prevent replay attacks)
const requestTime = parseInt(timestamp, 10);
const currentTime = Date.now();
const timeDiff = Math.abs(currentTime - requestTime);
const maxTimeDiff = 5 * 60 * 1000; // 5 minutes
if (isNaN(requestTime) || timeDiff > maxTimeDiff) {
logger.warn('Invalid or expired request timestamp', {
sovereignBankId,
timestamp,
timeDiff: `${timeDiff}ms`,
});
return false;
}
// Create payload for signing (must match client-side signing)
const payload = JSON.stringify({
method: req.method,
path: req.path,
body: req.body,
timestamp,
nonce,
});
try {
// Get sovereign identity to retrieve HSM key ID
const identity = await sovereignIdentityFabric.getIdentity(sovereignBankId, identityType);
if (!identity || !identity.hsmKeyId) {
logger.error('Sovereign identity not found or missing HSM key', {
sovereignBankId,
identityType,
});
return false;
}
// Verify signature using HSM
const isValid = await hsmService.verify(payload, signature, identity.hsmKeyId);
if (!isValid) {
logger.warn('Invalid request signature', {
sovereignBankId,
identityType,
keyId: identity.hsmKeyId,
});
}
return isValid;
} catch (error) {
logger.error('Error verifying request signature', {
sovereignBankId,
identityType,
error: error instanceof Error ? error.message : 'Unknown error',
});
return false;
}
}
/**
* Zero-Trust Authentication Middleware
* Enforces authentication and authorization for all requests
*/
export async function zeroTrustAuthMiddleware(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): Promise<void> {
try {
// Extract SIT token
const token = extractSitToken(req);
if (!token) {
throw new DbisError(
ErrorCode.UNAUTHORIZED,
'Missing or invalid Sovereign Identity Token'
);
}
// Verify JWT token
const jwtSecret = getEnv('JWT_SECRET');
if (!jwtSecret) {
logger.error('JWT_SECRET environment variable is not set');
throw new DbisError(
ErrorCode.INTERNAL_ERROR,
'Server configuration error: JWT secret not configured'
);
}
let decoded: JwtPayload;
try {
decoded = jwt.verify(token, jwtSecret) as JwtPayload;
} catch (error) {
logger.warn('JWT verification failed', {
error: error instanceof Error ? error.message : 'Unknown error',
});
throw new DbisError(ErrorCode.UNAUTHORIZED, 'Invalid or expired token');
}
// Extract claims shared by service and portal sessions
req.sovereignBankId = decoded.sovereignBankId;
req.employeeId = typeof decoded.employeeId === 'string' ? decoded.employeeId : undefined;
req.email = typeof decoded.email === 'string' ? decoded.email : undefined;
req.name = typeof decoded.name === 'string' ? decoded.name : undefined;
req.roleName = typeof decoded.roleName === 'string' ? decoded.roleName : undefined;
req.permissions = Array.isArray(decoded.permissions)
? decoded.permissions.filter((permission): permission is string => typeof permission === 'string')
: undefined;
req.sessionType = decoded.sessionType === 'portal' ? 'portal' : 'service';
req.portalSurface =
decoded.portalSurface === 'admin' ||
decoded.portalSurface === 'member' ||
decoded.portalSurface === 'core'
? decoded.portalSurface
: undefined;
req.identityType = decoded.identityType;
req.apiRole = decoded.apiRole;
if (isPortalSession(decoded)) {
if (!req.employeeId && !req.email) {
throw new DbisError(ErrorCode.UNAUTHORIZED, 'Invalid portal token payload');
}
} else {
if (!req.sovereignBankId) {
throw new DbisError(ErrorCode.UNAUTHORIZED, 'Invalid token payload');
}
// Verify request signature
const signatureValid = await verifyRequestSignature(
req,
req.sovereignBankId,
req.identityType || ''
);
if (!signatureValid) {
throw new DbisError(ErrorCode.UNAUTHORIZED, 'Invalid request signature');
}
}
// Check token expiration
if (decoded.exp && decoded.exp < Date.now() / 1000) {
throw new DbisError(ErrorCode.UNAUTHORIZED, 'Token expired');
}
next();
} catch (error) {
if (error instanceof DbisError) {
res.status(401).json({
success: false,
error: {
code: error.code,
message: error.message,
},
timestamp: new Date(),
});
} else {
res.status(500).json({
success: false,
error: {
code: ErrorCode.INTERNAL_ERROR,
message: 'Authentication error',
},
timestamp: new Date(),
});
}
}
}
/**
* Optional authentication middleware (for public endpoints)
*/
export function optionalAuthMiddleware(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void {
const token = extractSitToken(req);
if (token) {
try {
const jwtSecret = getEnv('JWT_SECRET');
if (jwtSecret) {
const decoded = jwt.verify(token, jwtSecret) as JwtPayload;
req.sovereignBankId = decoded.sovereignBankId;
req.employeeId = typeof decoded.employeeId === 'string' ? decoded.employeeId : undefined;
req.email = typeof decoded.email === 'string' ? decoded.email : undefined;
req.name = typeof decoded.name === 'string' ? decoded.name : undefined;
req.roleName = typeof decoded.roleName === 'string' ? decoded.roleName : undefined;
req.permissions = Array.isArray(decoded.permissions)
? decoded.permissions.filter((permission): permission is string => typeof permission === 'string')
: undefined;
req.sessionType = decoded.sessionType === 'portal' ? 'portal' : 'service';
req.portalSurface =
decoded.portalSurface === 'admin' ||
decoded.portalSurface === 'member' ||
decoded.portalSurface === 'core'
? decoded.portalSurface
: undefined;
req.identityType = decoded.identityType;
req.apiRole = decoded.apiRole;
}
} catch (error) {
// Ignore auth errors for optional auth
logger.debug('Optional auth failed (ignored)', {
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
next();
}