267 lines
8.2 KiB
TypeScript
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();
|
|
}
|