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"));
}
Source