import { vaultSecrets } from "@config/vault-secrets";
import { AuthGrpcService } from "@services/auth.service";
import { UserRole } from "@typez/auth";
import { getLogger } from "@utils/asyncLocalStorage";
import { SchemaUtils } from "@utils/schema";
import jwt from "jsonwebtoken";
/**
* Create authentication middleware for Express routes
*
* This middleware validates JWT tokens and optionally checks with the Auth Service
* for additional security. It attaches user information to the request object.
* @param options - Configuration options for the middleware
* @param options.jwtSecret - JWT secret for token validation
* @param options.validateWithAuthService - Whether to validate with Auth Service (optional)
* @param options.requiredRoles - Required user roles for access (optional)
* @returns Express middleware function
*/
/**
* Create authentication middleware for Express routes
* @category Middlewares
* @param {IAuthMiddlewareOptions} options Configuration options for the middleware
* @returns {Function} Express middleware function
*/
export function createAuthMiddleware(options) {
const logger = getLogger();
logger.info({ options }, "createAuthMiddleware");
return (req, res, next) => {
try {
// Extract Authorization header
const authHeader = req.headers.authorization;
const jwtSecret = vaultSecrets.get("JWT_SECRET");
if (!authHeader) {
res.status(401).json({
success: false,
message: "Authorization header is required",
error: "MISSING_AUTH_HEADER",
});
return;
}
// Validate header format and extract token
let token;
try {
SchemaUtils.validateAuthHeader(authHeader);
token = SchemaUtils.extractTokenFromHeader(authHeader);
}
catch {
res.status(401).json({
success: false,
message: "Invalid Authorization header format",
error: "INVALID_AUTH_HEADER",
});
return;
}
try {
const decoded = jwt.verify(token, jwtSecret);
const payload = SchemaUtils.validateJWTPayload(decoded);
const user = {
userId: payload.userId,
name: payload.name,
role: payload.role,
avatar: payload.avatar,
expiresAt: payload.exp ? new Date(payload.exp * 1000) : new Date(),
};
logger.info({ user }, "User information");
// Check required roles
if (options.requiredRoles && options.requiredRoles.length > 0) {
if (!options.requiredRoles.includes(user.role)) {
res.status(403).json({
success: false,
message: "Insufficient permissions",
error: "INSUFFICIENT_PERMISSIONS",
});
return;
}
}
const grpcClient = new AuthGrpcService(token);
req.grpcClient = grpcClient;
// Attach user and token to request
req.user = user;
next();
}
catch (error) {
logger.error({ error }, "Authentication error");
res.status(401).json({
success: false,
message: "Invalid token",
});
return;
}
}
catch (error) {
if (error instanceof AuthenticationError) {
res.status(error.status).json({
success: false,
message: error.message,
error: error.code,
});
}
}
};
}
// Custom error class for authentication
class AuthenticationError extends Error {
message;
status;
code;
constructor(message, status, code) {
super(message);
this.message = message;
this.status = status;
this.code = code;
this.name = "AuthenticationError";
}
}
// // Hybrid validation: Local first, fallback to Auth Service
// async function validateTokenHybrid(
// token: string,
// options: IAuthMiddlewareOptions,
// cacheDuration: number,
// ): Promise<IAuthUser> {
// // Try local validation first (fast)
// if (options.jwtSecret) {
// try {
// const decoded = jwt.verify(token, options.jwtSecret);
// const payload = SchemaUtils.validateJWTPayload(decoded);
// return {
// userId: payload.userId,
// name: payload.name,
// role: payload.role,
// avatar: payload.avatar,
// expiresAt: payload.exp ? new Date(payload.exp * 1000) : new Date(),
// };
// } catch (error) {
// logger.error(
// {
// error: error instanceof Error ? error.message : "Unknown error",
// },
// "Local validation failed, trying Auth Service",
// );
// // Local validation failed, try Auth Service
// // Silent fallback to Auth Service validation
// }
// }
// // Fallback to Auth Service validation
// // Check cache first
// const cached = validationCache.get(token);
// if (cached && Date.now() - cached.timestamp < cacheDuration) {
// return cached.result;
// }
// try {
// const authClient = AuthServiceClient.getInstance();
// const validationResult = await authClient.validateToken(token);
// if (!validationResult || !validationResult.isValid) {
// throw new AuthenticationError("Token validation failed", 401, "TOKEN_VALIDATION_FAILED");
// }
// const user: IAuthUser = {
// userId: validationResult.userId ?? "",
// name: validationResult.name ?? "",
// avatar: validationResult.avatar,
// role: validationResult.role
// ? UserReferenceUtils.protoValueToRole(validationResult.role as unknown as ProtoUserRole)
// : UserRole.SME,
// expiresAt: validationResult.expiresAt ?? new Date(),
// };
// // Cache the result
// validationCache.set(token, { result: user, timestamp: Date.now() });
// return user;
// } catch (error) {
// if (error instanceof AuthenticationError) {
// throw error;
// }
// throw new AuthenticationError("Auth Service unavailable", 503, "AUTH_SERVICE_ERROR");
// }
// }
/**
* Create middleware that requires admin role
* @category Middlewares
* @param {Omit<IAuthMiddlewareOptions, "requiredRoles">} options Configuration options (excluding requiredRoles)
* @returns {Function} Express middleware function that only allows admin users
*/
export function requireAdmin(options = {}) {
return createAuthMiddleware({
...options,
requiredRoles: [UserRole.ADMIN],
});
}
/**
* Create middleware that requires authentication (admin or SME role)
* @category Middlewares
* @param {IAuthMiddlewareOptions} options Configuration options (excluding requiredRoles)
* @returns {Function} Express middleware function that allows authenticated users
*/
export const requireAuthenticated = createAuthMiddleware({
requiredRoles: [UserRole.ADMIN, UserRole.SME],
});
/**
* Create optional authentication middleware
* @category Middlewares
* @param {IAuthMiddlewareOptions} options Configuration options for the middleware
* @returns {Function} Express middleware function that optionally authenticates
*/
export function optionalAuth(options) {
return (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
// No token provided, continue without authentication
next();
return;
}
// If token is provided, validate it
createAuthMiddleware(options)(req, res, next);
};
}
/**
* Export the authentication middleware functions
* @category Middlewares
* @returns {Record<string, Function>} Authentication middleware functions
*/
export default {
createAuthMiddleware,
requireAdmin,
requireAuthenticated,
optionalAuth,
};
Source