/** * 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 };