Agromind-backend / backend /middleware /authMiddleware.js
gh-action-hf-auto
auto: sync backend from github@32fb9685
8a6248c
/**
* Authentication Middleware
* Provides token verification and user population.
* Supports both Firebase ID tokens (RS256) and legacy HS256 JWTs.
*/
import jwt from "jsonwebtoken";
import User from "../models/auth.model.js";
/**
* Verify token (Firebase or legacy JWT) and populate req.user
*/
export const verifyToken = async (req, res, next) => {
try {
// Get token from cookie or Authorization header
let token = req.cookies?.token;
if (!token && req.headers.authorization) {
const authHeader = req.headers.authorization;
if (authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
}
}
if (!token) {
return res.status(401).json({
success: false,
message: "Access denied. No token provided.",
});
}
// Decode header to determine token type
const header = jwt.decode(token, { complete: true })?.header;
if (header?.kid) {
// Firebase ID token (RS256 with key ID) β€” verify via Firebase Admin
const admin = (await import('firebase-admin')).default;
if (admin.apps?.length > 0) {
const decodedToken = await admin.auth().verifyIdToken(token);
req.userId = decodedToken.uid;
req.userEmail = decodedToken.email;
req.firebaseUser = decodedToken;
// Look up the MongoDB user record for this Firebase/Google user
let mongoUser = await User.findOne({ firebaseUid: decodedToken.uid }).select('-password');
if (!mongoUser && decodedToken.email) {
mongoUser = await User.findOne({ email: decodedToken.email.trim().toLowerCase() }).select('-password');
// If found by email but missing firebaseUid, backfill it
if (mongoUser && !mongoUser.firebaseUid) {
mongoUser.firebaseUid = decodedToken.uid;
await mongoUser.save();
}
}
if (mongoUser) {
req.user = mongoUser;
req.userRole = mongoUser.role;
} else {
req.userRole = decodedToken.role || decodedToken.custom_claims?.role || 'farmer';
}
return next();
}
// Firebase Admin not initialised β€” decode WITHOUT verification to at least
// extract the uid so createListing can store sellerFirebaseUid.
// This is safe for optional-auth routes; verifyToken routes will reject above.
console.warn('[auth/verifyToken] Firebase token but Admin SDK not initialised β€” cannot verify.');
return res.status(401).json({
success: false,
message: "Firebase authentication unavailable.",
});
}
// Legacy HS256 JWT β€” verify with shared secret
const decoded = jwt.verify(token, process.env.JWT_KEY || process.env.JWT_SECRET);
// Populate req.user with user data from DB when available
const user = await User.findById(decoded.id).select("-password");
if (user) {
req.user = user;
}
req.userId = decoded.id;
req.userRole = decoded.role || user?.role || "farmer";
next();
} catch (error) {
console.error("Token verification error:", error.message);
if (error.name === "TokenExpiredError" || error.code === "auth/id-token-expired") {
return res.status(401).json({
success: false,
message: "Token expired. Please login again.",
});
}
if (error.name === "JsonWebTokenError") {
return res.status(401).json({
success: false,
message: "Invalid token.",
});
}
return res.status(401).json({
success: false,
message: "Authentication error.",
});
}
};
/**
* Optional auth β€” populates req.user / req.userId if a valid token is present,
* but NEVER blocks the request.
*
* Firebase token path:
* 1. If Firebase Admin SDK is initialised β†’ verify properly, set req.userId + req.user
* 2. If Firebase Admin SDK is NOT initialised β†’ decode the JWT payload WITHOUT
* signature verification (safe for optional-auth routes) and still set
* req.userId = uid. This ensures sellerFirebaseUid is always stored.
*
* Legacy HS256 JWT path: verify with JWT_KEY / JWT_SECRET, populate req.user.
*/
export const optionalAuth = async (req, res, next) => {
try {
let token = req.cookies?.token;
if (!token && req.headers.authorization) {
const authHeader = req.headers.authorization;
if (authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
}
}
if (token) {
const decoded_header = jwt.decode(token, { complete: true });
const header = decoded_header?.header;
if (header?.kid) {
// --- Firebase ID token ---
let firebaseUid = null;
let firebaseEmail = null;
try {
const admin = (await import('firebase-admin')).default;
if (admin.apps?.length > 0) {
// Proper full verification
const decodedToken = await admin.auth().verifyIdToken(token);
firebaseUid = decodedToken.uid;
firebaseEmail = decodedToken.email;
req.firebaseUser = decodedToken;
} else {
// Admin SDK not available β€” decode payload only (no signature check)
// This is intentionally unverified; it is safe here because
// optionalAuth never gates access, it only identifies the caller.
const payload = decoded_header?.payload;
firebaseUid = payload?.sub || payload?.user_id || null;
firebaseEmail = payload?.email || null;
console.warn('[auth/optionalAuth] Firebase Admin not initialised β€” using unverified uid:', firebaseUid);
}
} catch (firebaseErr) {
// Token expired or malformed β€” still try to extract uid gracefully
const payload = decoded_header?.payload;
firebaseUid = payload?.sub || payload?.user_id || null;
firebaseEmail = payload?.email || null;
console.warn('[auth/optionalAuth] Firebase token error, falling back to decoded uid:', firebaseErr.message);
}
if (firebaseUid) {
req.userId = firebaseUid;
req.userEmail = firebaseEmail;
// Try to link to a MongoDB user
let mongoUser = await User.findOne({ firebaseUid }).select('-password');
if (!mongoUser && firebaseEmail) {
mongoUser = await User.findOne({ email: firebaseEmail.trim().toLowerCase() }).select('-password');
if (mongoUser && !mongoUser.firebaseUid) {
mongoUser.firebaseUid = firebaseUid;
await mongoUser.save();
}
}
if (mongoUser) {
req.user = mongoUser;
req.userRole = mongoUser.role;
} else {
req.userRole = 'farmer';
}
}
return next();
}
// --- Legacy HS256 JWT ---
try {
const decoded = jwt.verify(token, process.env.JWT_KEY || process.env.JWT_SECRET);
const user = await User.findById(decoded.id).select("-password");
if (user) {
req.user = user;
req.userId = decoded.id;
req.userRole = decoded.role || user.role;
}
} catch {
// Invalid legacy token β€” ignore silently
}
}
next();
} catch (error) {
// Silently continue without auth
next();
}
};
/**
* Require specific role(s)
*/
export const requireRole = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: "Authentication required.",
});
}
if (!roles.includes(req.userRole)) {
return res.status(403).json({
success: false,
message: `Access denied. Required roles: ${roles.join(", ")}`,
});
}
next();
};
};
export default { verifyToken, optionalAuth, requireRole };