Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I handle errors in passport.deserializeUser()?

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 communicate back.

like image 473
Jason C Avatar asked Dec 09 '16 22:12

Jason C


1 Answers

I 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.

like image 195
Jason C Avatar answered Sep 22 '22 08:09

Jason C