Express Passport (node.js) error handling - node.js

I've looked at how error handling should work in node via this question Error handling principles for Node.js + Express.js applications?, but I'm not sure what passport's doing when it fails authentication. I have the following LocalStrategy:
passport.use(new LocalStrategy({ usernameField: 'email', passwordField: 'password' },
function(email, password, next) {
User.find({email: UemOrUnm}, function(err, user){
if (err) { console.log('Error > some err'); return next(err); }
if (!user) { console.log('Error > no user'); return next('Incorrect login or password'); }
if (password != user.password) {
return next(Incorrect login or password);
}
return next(null, user);
});
}
));
After I see 'Error > some err' console printout, nothing else happens. I would think it should continue on the the next path with an error parameter, but it doesn't seem to do that. What's going on?

The strategy-implementation works in conjunction with passport.authenticate to both authenticate a request, and handle success/failure.
Say you're using this route (which is passed an e-mail address and a password):
app.post('/login', passport.authenticate('local', {
successRedirect: '/loggedin',
failureRedirect: '/login', // see text
failureFlash: true // optional, see text as well
});
This will call the code in the strategy, where one of three conditions can happen:
An internal error occurred trying to fetch the users' information (say the database connection is gone); this error would be passed on: next(err); this will be handled by Express and generate an HTTP 500 response;
The provided credentials are invalid (there is no user with the supplied e-mail address, or the password is a mismatch); in that case, you don't generate an error, but you pass a false as the user object: next(null, false); this will trigger the failureRedirect (if you don't define one, a HTTP 401 Unauthorized response will be generated);
Everything checks out, you have a valid user object, so you pass it along: next(null, user); this will trigger the successRedirect;
In case of an invalid authentication (but not an internal error), you can pass an extra message along with the callback:
next(null, false, { message : 'invalid e-mail address or password' });
If you have used failureFlash and installed the connect-flash middleware, the supplied message is stored in the session and can be accessed easily to, for example, be used in a template.
EDIT: it's also possible to completely handle the result of the authentication process yourself (instead of Passport sending a redirect or 401):
app.post('/login', function(req, res, next) {
passport.authenticate('local', function(err, user, info) {
if (err) {
return next(err); // will generate a 500 error
}
// Generate a JSON response reflecting authentication status
if (! user) {
return res.send({ success : false, message : 'authentication failed' });
}
// ***********************************************************************
// "Note that when using a custom callback, it becomes the application's
// responsibility to establish a session (by calling req.login()) and send
// a response."
// Source: http://passportjs.org/docs
// ***********************************************************************
req.login(user, loginErr => {
if (loginErr) {
return next(loginErr);
}
return res.send({ success : true, message : 'authentication succeeded' });
});
})(req, res, next);
});

What Christian was saying was you need to add the function
req.login(user, function(err){
if(err){
return next(err);
}
return res.send({success:true});
});
So the whole route would be:
app.post('/login', function(req, res, next) {
passport.authenticate('local', function(err, user, info) {
if (err) {
return next(err); // will generate a 500 error
}
// Generate a JSON response reflecting authentication status
if (! user) {
return res.send(401,{ success : false, message : 'authentication failed' });
}
req.login(user, function(err){
if(err){
return next(err);
}
return res.send({ success : true, message : 'authentication succeeded' });
});
})(req, res, next);
});
source: http://passportjs.org/guide/login/

You need to add req.logIn(function (err) { }); and do the success redirect inside the callback function

Some time has passed and now the most right code will be:
passport.authenticate('local', (err, user, info) => {
if (err) {
return next(err); // will generate a 500 error
}
// Generate a JSON response reflecting authentication status
if (!user) {
return res.status(401).send({ error: 'Authentication failed' });
}
req.login(user, (err) => {
if (err) {
return next(err);
}
return res.status(202).send({ error: 'Authentication succeeded' });
});
});

I found this thread very useful!
https://github.com/jaredhanson/passport-local/issues/2
You could use this to return error and render it in form.
app.post('/login',
passport.authenticate('local', { successRedirect: '/home', failWithError: true }),
function(err, req, res, next) {
// handle error
return res.render('login-form');
}
);

This is what I got after console.log(req) at the failler route.
const localStrategy = new LocalStrategy({ usernameField: "email" }, verifyUser);
passport.use(localStrategy);
const authenticateWithCredentials = passport.authenticate("local", {
failureRedirect: "/api/auth/login-fail",
failureMessage: true,
});
validation method find your user from db and throw error to the cb if there is any
const verifyUser = async (email, password, cb) => {
const user = await User.findOne({ email });
if (!user) return cb(null, false, { message: "email/password incorrect!" });
const isMatched = await user.comparePassword(password);
if (!isMatched)
return cb(null, false, { message: "email/password incorrect!" });
cb(null, {
id: user._id,
email,
name: user.name,
});
};
now setup your route
router.post("/sign-in", authenticateWithCredentials,(req, res) => {
res.json({user: req.user})
});
router.get("/login-fail", (req, res) => {
let message = "Invalid login request!";
// if you are using typescript cast the sessionStore to any
const sessions = req.sessionStore.sessions || {};
for (let key in sessions) {
const messages = JSON.parse(sessions[key])?.messages;
if (messages.length) {
message = messages[0];
break;
}
}
res.status(401).json({ error: message });
});

Related

Passportjs sessions fail when JSON response is set first

I'm using express + passport to handle authentication for a react app. Because I'm using react, I don't want to automatically redirect the login request via express, instead I want to return JSON.
However, I've noticed that simply returning JSON and then calling next(null, user) seems to prevent passport from running the serializeUser method. By calling next passport should automatically call the req.login function, which doesn't appear to happen when I've sent json.
It appears that if I manually call req.login and then send the JSON, it works (the serialize method is called, the user has a session).
Is there a way to send a json response and call next without having to manually invoke req.login? Feels like I'm just not doing things the correct way.
module.exports = function(app) {
app.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user) => {
if (err) {
res.json({ errorMessage: err.message });
return next(err);
}
if (!user) {
res.json({ errorMessage: 'Invalid user.' });
return next(err);
}
// This fails to invoke the session serialization:
// res.json({
// success: true,
// username: user.username
// });
// return next(null, user);
// This works:
req.logIn(user, loginErr => {
if (loginErr) {
return next(loginErr);
}
res.json({
success: true,
username: user.username
});
return;
});
})(req, res, next);
});
};

Handle rejection of passport.authenticate() function

I have a router like this:
/* get user info */
router.get('/info', authenticate(), function (req, res) {
models.User.getUserById(req.user.id, function (result) {
if (result) {
var user = result.dataValues;
res.json({
success: true,
data: user,
message: 'Get user info success!'
})
} else {
res.json({
success: false,
message: 'User does not exist!'
})
}
})
});
the authenticate() function is like this:
/* ensure authentication */
var authenticate = function () {
return passport.authenticate('jwt', {
session: false
});
}
If user logged in, the router would go to function(req, res). But what if user has not logged in yet ? How can I return to client a message if user has not logged in ? Can I put a callback to passport.authenticate() ? If I can, what is the parameters of this callback ?
According to Docs, you can add a failure redirect URL and a failure flash message like this :
var authenticate = function () {
return passport.authenticate('jwt', {
session: false,
failureRedirect: '/login',
failureFlash: 'Invalid username or password.'
});
}
Or if you want a custom callback :
var authenticate = function (req, res, next) {
return passport.authenticate('jwt', function (err, user, info) {
if (err) { return next(err); }
if (!user) {
// code if failed here
return res.redirect('/login')
}
req.logIn(user, function (err) {
if (err) { return next(err); }
return res.redirect('/users/' + user.username)
})
})
}

Passport send callbacks for errors to be able to res.json

How can i catch errorfrom local-signup? i cant find a way to catch them.
The idea is to catch the errors, then send them as a message back to client with
res.json
Current output:
already taken????????????????????
node.js
router.post('/register', passport.authenticate('local-signup'),function(req, res, next) {
console.log("registration");
console.log("ERROR?");
console.log(req);
console.log(res);
// res.json({type: 'danger',message: info});
});
passport:
passport.use('local-signup', new LocalStrategy({
usernameField : 'username',
passwordField : 'password',
passReqToCallback : true
},
function(req, username, password, done) {
process.nextTick(function() {
console.log("doing local signup");
Account.findOne({username : username }, function(err, user) {
if (err)
return done(err);
if (user) {
console.log("already taken????????????????????");
return done(null, false, { message: 'That username is already taken.'});
return done(err);
} else {
var newUser = new Account();
newUser.username = username;
newUser.password = newUser.encryptPassword(password);
// save the user
newUser.save(function(err) {
if (err)
throw err;
return done(null, newUser);
});
}
});
});
}));
Update:
I tried custom error callback code, but then i cant get req property to send request back to client.
I also tried to move the authenticate middleware to be called within the function req, res, next, but then it wont be called at all.
Do you have a generic error handler in your app? If you do, you could pass the option failWithError to the LocalStrategy.
Example:
passport.authenticate('local-signup', { failWithError: true })
passport code snipped:
...
if (options.failWithError) {
return next(new AuthenticationError(http.STATUS_CODES[res.statusCode], rstatus));
}
...
And in case, when error occurs, passport will pass the error for the error handler.
Error handler example:
...
// it has to be the last operation in the app object
app.use(errorHandler);
function errorHandler(err, req, res, next) {
if(typeof error == AuthenticationError) {
res.status(403);
res.json('error', { error: err });
}
else{ /* anything else */}
}
Hope it help you.

How to show custom error messages using passport and express

I check if the email already exist when registering a user. If a user already exist I pass the error message "email already exists". But in the front end it shows "Unauthorized" error 401. I want to pass the error message which I pass to the front end from the back end but it pass the default message. below is how I check if a user already exist and send the error message,
exports.register = new LocalStrategy({
usernameField: 'email',
passReqToCallback: true
}, function(req, email, password, done,res) {
var newUser = new User({
email: email,
password: password,
name: req.body.fname
});
var searchUser = {
email: email
};
User.findOne(searchUser, function(err, user) {
if (err) return done(err);
console.log("err "+err);
if (user) {
return done(null, false, {
message: "email already exists"
});
}
newUser.save(function(err) {
console.log("newUser "+newUser);
done(null, newUser);
})
});
});
I use passport for authentication,
authRouter.post('/signup', passport.authenticate('local-register'),function(req, res) {
createSendToken(req.user, res);
});
The error message is not the one I pass to the front end. It shows unauthorized error, which is the default error. When I print the error message in the console in front end it shows,
Object {data: "Unauthorized", status: 401, config: Object, statusText: "Unauthorized"}
You are not saying what output you want in your front end, but I'm guessing you want to have data to be the message that you set in your LocalStrategy.
Here is one way to do that:
authRouter.post('/signup', function(req, res, next) {
passport.authenticate('local-register', function(err, user, info) {
if (err) { return next(err); }
if (!user) {
res.status(401);
res.end(info.message);
return;
}
createSendToken(req.user, res);
})(req, res, next);
});
You can take advantage of passReqToCallback: true in passport.
With this option enabled, req will be passed as the first argument to the verify callback.
Also by taking advantage of flash messages.
Here is basic example of how you use it,
// your strategy
passport.use(new LocalStrategy({
passReqToCallback: true
},
(req, username, password, done) => {
User.findOne({ username: username }, (err, user) => {
if (err) done(err)
if (!user) {
console.log('user does not exists')
return done(null, false, req.flash('message', 'User does not exist' ))
} else {
console.log('user exists')
return done(null, user, req.flash('message', 'User exist'))
}
})
}
))
// your GET login
router.get("/login", (req, res) => {
var message = req.flash('message')
res.render("pages/login", { message })
})
As mentioned in other answers you can manually check the info parameter and then return your own message based on that:
// ❌ MESSY ❌
authRouter.post('/signup', function(req, res, next) {
passport.authenticate('local-register', function(err, user, info) {
if(info.name === 'UserExistsError' ) {
return done(null, false, {
message: "Email already exists"
});
} else if (info.name === 'IncorrectUsernameError') {
return done(null, false, {
message: "Email does not exist"
});
} else if(....
But A MUCH cleaner way is to just specify custom error messages when you create the Account Mongoose model:
var Account = new Schema({
...
});
var options = {
errorMessages: {
UserExistsError: 'Email already exists',
IncorrectUsernameError: 'Email does not exist',
IncorrectPasswordError: ...
}
};
Account.plugin(passportLocalMongoose, options);
Then in your signup route you can simply return the info.message to the user.
// ✨ CLEAN ✨
authRouter.post('/signup', function(req, res, next) {
passport.authenticate('local-register', function(err, user, info) {
return done(null, false, {
message: info.message
});
});
});
If you are using passReqToCallback = true then you can send it just like
if (!isMatch) { return done({ message: 'Incorrect password' }); }

How to handle passport authentication response and show it to the user

I'm authenticating my nodeJs app using passport local strategy. Everything is working fine. But how can I show the user appropriate message that he has entered invalid login credentials. My present code is just sending 401 unauthorized error on screen.
Here is my code
passport.use(new LocalStrategy(function(username, password, callback) {
User.findOne({
username : username
}, function(err, user) {
if (err) {
return callback(err);
}
// No user found with that username
if (!user) {
return callback(null, false);
}
// Make sure the password is correct
user.verifyPassword(password, function(err, isMatch) {
if (err) {
return callback(err);
}
// Password did not match
if (!isMatch) {
return callback(null, false);
}
// Success
return callback(null, user);
});
});
}));
exports.isLocalAuthenticated = passport.authenticate('local', {
session : true
});
router.post('/', authController.isLocalAuthenticated, function(req, res) {
//here I want to show the error message to user
});
The documentation has clearly described your case under Custom Callback section.
You need to add custom callback like this:
exports.isLocalAuthenticated = function(req, res, next) {
passport.authenticate('local', function(err, user, info) {
if (err) { return next(err); } //error exception
// user will be set to false, if not authenticated
if (!user) {
res.status(401).json(info); //info contains the error message
} else {
// if user authenticated maintain the session
req.logIn(user, function() {
// do whatever here on successful login
})
}
})(req, res, next);
}
You don't need to specify the latter callback.

Resources