Source

grpc/interceptors/auth.interceptor.js

import grpc from "@grpc/grpc-js";
import { logger } from "../../config/logger.js";
import { User } from "../../models/User.model.js";
import { JwtUtils } from "../../utils/jwt.utils.js";
import { UserRole } from "../auth.js";
// List of public methods that don't require authentication
const PUBLIC_METHODS = ["/auth.Auth/Login", "/auth.Auth/GetUserById"];
/**
 * Create a ServiceError with required metadata field
 */
function createServiceError(code, message, details) {
    return Object.assign(new Error(message), {
        code,
        details: details || message,
        metadata: new grpc.Metadata(),
    });
}
/**
 * gRPC Server Interceptor for Authentication
 *
 * This interceptor is called for all RPC methods and handles authentication
 * by extracting JWT tokens from metadata and attaching user info to the call.
 *
 * Based on gRPC proposal L112: Node.js Server Interceptors
 * @see https://github.com/grpc/proposal/blob/master/L112-node-server-interceptors.md
 */
export const authInterceptor = (methodDescriptor, call) => {
    const methodPath = methodDescriptor.path;
    // Skip authentication for public methods
    if (PUBLIC_METHODS.includes(methodPath)) {
        logger.debug({ method: methodPath }, "Public method, skipping authentication");
        return new grpc.ServerInterceptingCall(call);
    }
    let interceptingCallRef = null;
    const Listener = new grpc.ServerListenerBuilder()
        .withOnReceiveMetadata(function (metadata, next) {
        void (async () => {
            try {
                const authHeader = metadata.get("authorization")[0];
                if (!authHeader) {
                    logger.warn({ method: methodPath }, "Missing authorization header in gRPC call");
                    throw createServiceError(grpc.status.UNAUTHENTICATED, "Authorization header is required");
                }
                // Extract token from "Bearer <token>" format
                const tokenMatch = authHeader.match(/^Bearer\s+(.+)$/i);
                if (!tokenMatch) {
                    logger.warn({ method: methodPath }, "Invalid authorization header format");
                    throw createServiceError(grpc.status.UNAUTHENTICATED, "Invalid authorization header format. Expected: Bearer <token>");
                }
                const token = tokenMatch[1];
                if (!token) {
                    logger.warn({ method: methodPath }, "Empty token in authorization header");
                    throw createServiceError(grpc.status.UNAUTHENTICATED, "Invalid token");
                }
                // Verify the token
                const decoded = JwtUtils.verifyAccessToken(token);
                // Fetch user from database
                const dbUser = await User.findById(decoded.userId).select("-passwordHash");
                if (!dbUser) {
                    logger.warn({ userId: decoded.userId, method: methodPath }, "User not found");
                    throw createServiceError(grpc.status.UNAUTHENTICATED, "User not found");
                }
                // Attach user info to the original call context
                call.user = {
                    id: dbUser._id.toString(),
                    email: dbUser.email,
                    name: dbUser.name,
                    role: dbUser.role === "admin" ? UserRole.admin : UserRole.sme,
                    avatar: dbUser.avatar,
                };
                metadata.set("user", JSON.stringify({
                    id: dbUser._id.toString(),
                    email: dbUser.email,
                    name: dbUser.name,
                    role: dbUser.role === "admin" ? UserRole.admin : UserRole.sme,
                    avatar: dbUser.avatar,
                }));
                logger.debug({
                    userId: dbUser._id.toString(),
                    email: dbUser.email,
                    method: methodPath,
                }, "User authenticated in gRPC call");
                // Continue to the next interceptor or handler
                next(metadata);
            }
            catch (error) {
                logger.error({
                    error: error instanceof Error ? error.message : "Unknown error",
                    method: methodPath,
                }, "Authentication failed in gRPC call");
                // Convert to gRPC error
                let grpcError;
                if (error.code !== undefined) {
                    grpcError = error;
                }
                else if (error instanceof Error && error.name === "TokenExpiredError") {
                    grpcError = createServiceError(grpc.status.UNAUTHENTICATED, "Token expired");
                }
                else if (error instanceof Error && error.name === "JsonWebTokenError") {
                    grpcError = createServiceError(grpc.status.UNAUTHENTICATED, "Invalid token");
                }
                else {
                    grpcError = createServiceError(grpc.status.INTERNAL, "Authentication failed");
                }
                // Send error to client instead of throwing
                if (interceptingCallRef) {
                    interceptingCallRef.sendStatus({
                        code: grpcError.code ?? grpc.status.UNAUTHENTICATED,
                        details: grpcError.message || "Authentication failed",
                        metadata: grpcError.metadata ?? new grpc.Metadata(),
                    });
                }
            }
        })();
    })
        .withOnReceiveMessage(function (message, next) {
        next(message);
    })
        .build();
    // Create intercepting call that will handle authentication
    const interceptingCall = new grpc.ServerInterceptingCall(call, {
        start: (listener) => {
            listener(Listener);
        },
    });
    interceptingCallRef = interceptingCall;
    return interceptingCall;
};
/**
 * Role-based authorization checker
 * Use this in your RPC handlers to check if user has required role
 */
export function requireRole(call, allowedRoles) {
    if (!call.user) {
        return false;
    }
    return allowedRoles.includes(call.user.role);
}
/**
 * Admin-only checker
 * Convenience function to check if user is admin
 */
export function requireAdmin(call) {
    return requireRole(call, [UserRole.admin]);
}
/**
 * Get authenticated user from call
 * Returns user if authenticated, throws ServiceError if not
 * Use this at the beginning of any protected RPC handler
 *
 * @example
 * const user = getAuthenticatedUser(call);
 * // user is guaranteed to exist or error is thrown
 */
export function getAuthenticatedUser(call) {
    return JSON.parse(call.metadata.get("user"));
}