I've got Express and Passport configured like so:
var express = require("express");
var site = express();
var flash = require("connect-flash");
var passport = require("passport");
site.use(require("cookie-parser")());
site.use(require("body-parser").urlencoded({extended:false}));
site.use(require("express-session")(...));
site.use(flash());
site.use(passport.initialize());
site.use(passport.session());
I've got a pretty stock implementation of deserializeUser
(I'm using local
authentication backed by MySQL via Bookshelf):
var db = require("./database.js"); // exports models e.g. db.User
passport.deserializeUser(function(id, done) {
db.User.findById(id).then(function (user) {
done(null, user);
}).catch(function (err) {
done(err, null);
});
});
I'm running in to the following specific problem: When a logged-in user is deleted from the database (happens when e.g. a site administrator deletes the user), the deserialization fails, as expected, with a CustomError("EmptyResponse")
from Bookshelf. However, I don't know how to handle it; done(err, null)
ultimately just causes the error message and stack trace to get sent back as HTML to the client.
The question is: How can I provide custom, graceful error handling from deserializeUser
on a failure?
Now, if it simplifies things, I could add {require:false}
to the db.User.findById
call to give me a null
user instead of an error, but I still don't know how to handle it (and also I still do need to handle error objects e.g. if the database server is down and there's a connection error).
The action I want to take on failure is to redirect the user back to the login page, potentially with a descriptive flash message, but I don't have any access to the request/response in deserializeUser
and I'm not sure how to municate back.
I've got Express and Passport configured like so:
var express = require("express");
var site = express();
var flash = require("connect-flash");
var passport = require("passport");
site.use(require("cookie-parser")());
site.use(require("body-parser").urlencoded({extended:false}));
site.use(require("express-session")(...));
site.use(flash());
site.use(passport.initialize());
site.use(passport.session());
I've got a pretty stock implementation of deserializeUser
(I'm using local
authentication backed by MySQL via Bookshelf):
var db = require("./database.js"); // exports models e.g. db.User
passport.deserializeUser(function(id, done) {
db.User.findById(id).then(function (user) {
done(null, user);
}).catch(function (err) {
done(err, null);
});
});
I'm running in to the following specific problem: When a logged-in user is deleted from the database (happens when e.g. a site administrator deletes the user), the deserialization fails, as expected, with a CustomError("EmptyResponse")
from Bookshelf. However, I don't know how to handle it; done(err, null)
ultimately just causes the error message and stack trace to get sent back as HTML to the client.
The question is: How can I provide custom, graceful error handling from deserializeUser
on a failure?
Now, if it simplifies things, I could add {require:false}
to the db.User.findById
call to give me a null
user instead of an error, but I still don't know how to handle it (and also I still do need to handle error objects e.g. if the database server is down and there's a connection error).
The action I want to take on failure is to redirect the user back to the login page, potentially with a descriptive flash message, but I don't have any access to the request/response in deserializeUser
and I'm not sure how to municate back.
-
Can you just check for the message in the error (
err.message
) and send back some custom response that looks nice for the user? (In thecatch
callback) – Josh Beam Commented Dec 9, 2016 at 22:06 -
@JoshBeam That's what I want to do, but how do I access the response object from there? It's not passed as a parameter to
deserializeUser
. – Jason C Commented Dec 9, 2016 at 22:06
2 Answers
Reset to default 17I figured out one solution. Errors here can be handled in an Express middleware error-handler. So, for example:
// Note: Must be used *after* passport middleware.
site.use(function(err, req, res, next) {
if (err) {
// Handle deserialization errors here.
} else {
next();
}
});
One important caveat though is if deserialization failed non-recoverably, all routes accessed through passport (even ones that don't require authentication) will continue to fail, which most likely also includes your login and logout endpoints, thus permanently breaking access until the user clears their cookies. So you have to force a logout, it's the only real way to recover:
site.use(function(err, req, res, next) {
if (err) {
req.logout(); // So deserialization won't continue to fail.
} else {
next();
}
});
Further building on that, in my case I wanted to redirect back to the login page with a flash message. But you have to be careful: If an error occurs on the login page itself you need to avoid infinite redirects, so:
site.use(function(err, req, res, next) {
if (err) {
req.logout();
if (req.originalUrl == "/loginpage") {
next(); // never redirect login page to itself
} else {
req.flash("error", err.message);
res.redirect("/loginpage");
}
} else {
next();
}
});
And of course you probably want to only do this for CustomError("EmptyResponse")
, or even redo the error handling in your deserialize implementation to have it throw your own more specific error.
Kind of a weird approach, but seems to get the job done. Open to cleaner suggestions.
In the Passport docs "Configure" section, they show the ability to send a specific message:
return done(null, false, { message: 'Incorrect username.' });
The above example is in their docs, but in your case, you could do something like:
passport.deserializeUser(function(id, done) {
db.User.findById(id).then(function (user) {
done(null, user);
}).catch(function (err) {
done(err, null, { message: 'User does not exist' });
});
});