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
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,
});
});