Search code examples
javascriptnode.jsexpressbackend

Express.js: Too Many Redirects Issue When Implementing Role-Based Authentication with Passport.js


Encountering an issue with Express.js and Passport.js while implementing role-based authentication. Despite setting up role-specific middleware, I'm facing a 'Too Many Redirects' error. The application involves redirecting users to their respective dashboards based on their role (e.g., admin, recruiter). How can I resolve this redirection loop and ensure proper authentication flow?

What I Tried: I implemented role-based authentication in my Express.js application using Passport.js. I defined middleware functions to check if a user is authenticated and to redirect them to their respective dashboards based on their role (recruteur, users, or admin). I also configured Passport.js to authenticate users based on their role during login.

passport.config.js file :

const LocalStrategy = require("passport-local").Strategy;
const db = require("./db");
const bCrypt = require("bcrypt");

async function initializePassport(passport) {
  async function authUser(email, password, formUserType, done) {
    try {
      let userType;
      switch (formUserType) {
        case "recruteur":
          userType = "recruteur";
          break;
        case "users":
          userType = "users";
          break;
        case "admin":
          userType = "admin";
          break;
        default:
          throw new Error("this user type does not exist");
      }

      const query = `SELECT * FROM ${userType} WHERE email = $1`;
      const { rows } = await db.query(query, [email]);
      const user = rows[0];

      if (!user) {
        return done(null, false, { message: "Incorrect email address" });
      }

      const passwordMatched = await bCrypt.compare(password, user.mot_de_passe);
      if (!passwordMatched) {
        return done(null, false, { message: "Incorrect password" });
      }

      user.userType = userType;
      return done(null, user, userType);
    } catch (error) {
      done(error);
    }
  }

  passport.use(
    new LocalStrategy(
      {
        usernameField: "email",
        passReqToCallback: true,
      },
      async (req, email, password, done) => {
        await authUser(email, password, req.body.userType, done);
      }
    )
  );

  passport.serializeUser((user, done) => {
    let userId;
    let userType;
    switch (user.userType) {
      case "recruteur":
        userId = user.recruteur_id;
        userType = "recruteur";
        break;
      case "users":
        userId = user.users_id;
        userType = "users";
        break;
      case "admin":
        userId = user.admin_id;
        userType = "admin";
        break;
      default:
        return done(new Error("Unknown user type"));
    }

   
    
    done(null, { id: userId, type: userType });
  });

  passport.deserializeUser(async (idObj, done) => {
    try {
      const { id, type } = idObj;
      let userType;
      switch (type) {
        case "recruteur":
          userType = "recruteur";
          break;
        case "users":
          userType = "users";
          break;
        case "admin":
          userType = "admin";
          break;
        default:
          return done(new Error("Unknown user type"));
      }
  
      const query = `SELECT * FROM ${userType} WHERE ${userType}_id = $1`;
      const { rows } = await db.query(query, [id]);
      const user = rows[0];
  
      if (!user) {
        return done(new Error("User not found"));
      }

      user.userType = userType;
      return done(null, user, userType);
    } catch (err) {
      done(err);
    }
  });
  
}

module.exports = initializePassport;

app.js file with my middleware checkAuthenticaded, checkNotAuthenticated

if (process.env.NODE_ENV !== "production") {
  require("dotenv").config();
}
const initializePassport = require("./config/passport.config");

const express = require("express");
const passport = require("passport");
const session = require("express-session");

const path = require("path");
const helmet = require("helmet");
const flash = require("express-flash");
const methodOverride = require('method-override');


const logger = require("morgan");

const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(flash());

//initialise user session
app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: true,
    cookie: {
      maxAge: 3 * 60 * 60 * 1000,
      httpOnly: true 
    }
  })
);

app.use(passport.session() );
app.use(passport.initialize());


app.use(methodOverride('_method'))

//auth the user in the DB
initializePassport(passport);

const checkAuthenticated = (req, res, next) => {
  if (req.isAuthenticated()) {
    const userType = req.user.userType;
    console.log('Role --------------------------------------------------->',userType);
    if (userType === 'recruteur') {
      return res.redirect("/recruter/dashboard");
    } else if (userType === 'users') {
      return res.redirect("/users/dashboard");
    } else if (userType === 'admin') {
      return res.redirect("/admin/dashboard");
    } else {
      return res.redirect("/login");
    }
  } else {
    return res.redirect("/login");
  }
};





const checkNotAuthenticated = (req, res, next) => {
  if (!req.isAuthenticated()) {
    return next(); 
  } else {
    const userType = req.user.userType;
    switch (userType) {
      case 'recruteur':
        return res.redirect("/recruter/dashboard");
      case 'users':
        return res.redirect("/users/dashboard");
      case 'admin':
        return res.redirect("/admin/dashboard");
      default:
        return res.redirect("/login");
    }
  }
};

const staticFilesPath = path.join(__dirname, 'views');

app.use(helmet());
app.use(express.json());

app.use(express.static(path.join(__dirname, "views")));
app.use("views", express.static(path.join(__dirname, "views", "public")));
app.use(express.static(staticFilesPath, { type: 'application/javascript' }));


app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));

app.use(logger("tiny"));

module.exports = { app, checkAuthenticated, checkNotAuthenticated };

server.js file :

if (process.env.NODE_ENV !== 'production') {
  require("dotenv").config();
}

const offersRoute = require("./routes/offers/offers.route");
const homeRouter = require('./routes/home/home.route');
const offerRoute = require('./routes/offers/offer.route');
const authRoute = require('./routes/auth/auth.route');
const recruterRoute = require('./routes/recruter/recruter.route');
const usersRoute = require('./routes/users/users.route');
const unauthorizedRoute = require('./routes/auth/unauthorized.route');

const http = require("http");


const { app } = require("./app");

const PORT = process.env.PORT;

const server = http.createServer(app);

app.use('/home', homeRouter); 
app.use("/offers", offersRoute);
app.use("/offer", offerRoute);
app.use("/login", authRoute);
app.use("/recruter", recruterRoute);
app.use('/users', usersRoute);
app.use('/unauthorized', unauthorizedRoute)

server.listen(PORT, () => {
  console.log(`You are listening to port ${PORT}...`);
});

auth.route.js file :

const express = require("express");
const passport = require("passport");
const flash = require("express-flash");

const authRoute = express.Router();
const {
  postNewRecruterAuth,
  validate,
  recruterAuthValidationRule,
  postNewUserAuth,
  userAuthValidationRule,
} = require("../../controllers/auth.controller");
const { checkAuthenticated, checkNotAuthenticated } = require("../../app");

//User render login routes
authRoute.get("/", (req, res) => {
  res.render("auth/users/login", {
    title: "Connectez vous.",
  });
});

//User register routes
authRoute.get("/register", (req, res) => {
  res.render("auth/users/register", {
    title: "Créer un compte utilisateur",
  });
});

//User authentification routes
authRoute.post(
  "/",
  passport.authenticate("local", {
    successRedirect: "/users/dashboard",
    failureRedirect: "/login",
    failureFlash: true,
  })
);


//user registration route
authRoute.post(
  "/register",
  userAuthValidationRule(),
  validate,
  postNewUserAuth
);




//Recruter Auth routes
authRoute.get("/recruter", checkNotAuthenticated, (req, res) => {
  //recruter login
  res.render("auth/recruter/recruter_login", {
    title: "Connectez vous en tant que recruteur.",
    messages: req.flash("error"),
  });
});

//recruter login route
authRoute.post(
  "/recruter",
  passport.authenticate("local", {
    successRedirect: "/recruter/dashboard",
    failureRedirect: "/login/recruter",
    failureFlash: true,
  })
);

authRoute.get("/recruter/register", checkNotAuthenticated, (req, res) => {
  //recruter register
  res.render("auth/recruter/recruter_register", {
    title: "Créer un compte recruteur",
  });
});

authRoute.post(
  "/recruter/register",
  recruterAuthValidationRule(),
  validate,
  postNewRecruterAuth
);

//Admin Auth routes

module.exports = authRoute;

At the moment, I'm testing the login functionality specifically for recruteurs in my Express.js application. However, when a recruteur successfully logs in, instead of being redirected to the recruteur dashboard ("/recruter/dashboard"), the application enters into an infinite redirection loop.

recruter.route.js file that I am actualy testing :

const express = require("express");
const recruterRoute = express.Router();
const passport = require("passport");
const { checkAuthenticated } = require("../../app");

recruterRoute.get("/", (req, res) => {
  res.render("layouts/recruter/recruter_page", {
    title: "Jobify pour recruteur",
  });
});

recruterRoute.post("/dashboard/logout", (req, res) => {
  req.logout(() => {
    res.redirect("/login/recruter");
  });
});

recruterRoute.get("/dashboard", checkAuthenticated,   (req, res) => {
  res.render("layouts/recruter/recruter_dashboard", {
    title: "Votre tableau de bord",
    user: req.user,
  });
});

module.exports = recruterRoute;

What I Expected: I expected that when a user logs in, they would be redirected to the appropriate dashboard based on their role. For example, recruteurs should be redirected to "/recruter/dashboard", users to "/users/dashboard", and admin to "/admin/dashboard". I also expected that the authentication process would work seamlessly without any redirection loops.

What Actually Resulted: However, when I tested the application, I encountered a "Too Many Redirects" error. It seems that there is a redirection loop happening somewhere in the authentication process, but I'm not sure where the issue lies. Despite setting up role-specific middleware and configuring Passport.js to handle role-based authentication, the redirection loop persists, and I'm unable to resolve it.

this is the infinite loop that I get in my log:

GET /login/recruter 200 14109 - 9.306 ms
POST /login/recruter 302 82 - 104.179 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.246 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.197 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.033 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.213 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 0.997 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.092 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.248 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.335 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.232 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.296 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.186 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.198 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 0.775 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.093 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.145 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.099 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.038 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 0.684 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.291 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 0.575 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.015 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 0.837 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 0.956 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 0.713 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 0.973 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 0.684 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 0.953 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.527 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 0.906 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 0.907 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 0.629 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.024 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 3.310 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.657 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.030 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.575 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.036 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.140 ms
Role ---------------------------------------------------> recruteur
GET /recruter/dashboard 302 82 - 1.440 ms

Solution

  • The issue is that the checkAuthenticated middleware, which is in front of all of the authentication-requiring route handlers, never calls next() to allow the handler to actually handle the request. Instead, it always performs a redirect.

    It appears that you are attempting to implement roles for your access control. I would suggest changing your authentication middleware like this:

    const checkAuthenticated = (requiredRole) => (req, res, next) => {
      if (!req.isAuthenticated()) {
        return res.redirect("/login");
      }
    
      const userType = req.user.userType;
      if (userType !== requiredRole) {
        return redirectToRoleHomePage(req, res);
      }
    
      // user is authenticated and has the correct role,
      // pass control to the handler from this middleware
      return next();
    };
    
    const checkNotAuthenticated = (req, res, next) => {
      if (req.isAuthenticated()) {
        return redirectToRoleHomePage(req, res);
      }
    
      // user is *not* authenticated, pass control to the handler
      return next();
    };
    
    const redirectToRoleHomePage = (req, res) => {
      const userType = req.user.userType;
      switch (userType) {
        case "recruteur":
          return res.redirect("/recruter/dashboard");
        case "users":
          return res.redirect("/users/dashboard");
        case "admin":
          return res.redirect("/admin/dashboard");
        default:
          return res.redirect("/login");
      }
    };
    

    Now, the check* middleware redirect if their conditions are not met, otherwise they call next(). I've changed checkAuthenticated to take a requiredRole parameter - this allows you to specify at the handler level which role is required by that handler. With this, you can change your handlers like:

    recruterRoute.get("/", (req, res) => {
      res.render("layouts/recruter/recruter_page", {
        title: "Jobify pour recruteur",
      });
    });
    
    recruterRoute.post("/dashboard/logout", checkAuthenticated('recruteur'), (req, res) => {
      req.logout(() => {
        res.redirect("/login/recruter");
      });
    });
    
    recruterRoute.get("/dashboard", checkAuthenticated('recruteur'), (req, res) => {
      res.render("layouts/recruter/recruter_dashboard", {
        title: "Votre tableau de bord",
        user: req.user,
      });
    });
    

    For other roles, you'd use checkAuthenticated('users'), checkAuthenticated('admin'), etc.

    Alternatively, if you want all routes inside recruterRoute to check that the user has logged in and is a recruteur, you could just apply the middleware in the recruterRoute Router itself so that you don't need to remember it for every route:

    const recruterRoute = express.Router();
    
    recruterRoute.use(checkAuthenticated('recruteur'));
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    // middleware applied with `.use()` before the routes
    // -> gets called before any route below
    
    recruterRoute.get("/", (req, res) => {
      res.render("layouts/recruter/recruter_page", {
        title: "Jobify pour recruteur",
      });
    });
    
    recruterRoute.post("/dashboard/logout", (req, res) => {
      req.logout(() => {
        res.redirect("/login/recruter");
      });
    });
    
    recruterRoute.get("/dashboard", (req, res) => {
      res.render("layouts/recruter/recruter_dashboard", {
        title: "Votre tableau de bord",
        user: req.user,
      });
    });