Sending back a JSON response when failing Passport.js authentication

57,232

Solution 1

I believe the callback function that your 'authenticate' static calls (called 'callback' in your code) accepts a 3rd parameter - "info" - which your code can provide. Then, instead of passing in the { failureRedirect: ...} object, pass in a function which takes 3 arguments - err, user, and info. The "info" you provided in your authenticate method will be passed to this callback.

Passport calls this scenario "custom callback". See the docs here: http://passportjs.org/guide/authenticate/

Solution 2

I had a similar issue with Passport and failed login responses. I was building an API, and wanted all responses to be returned as JSON. Passport responds to an invalid password with status: 401 and body: Unauthorized. That's just a text string in the body, not JSON, so it broke my client which expected all JSON.

As it turns out, there is a way to make Passport just return the error to the framework instead of trying to send a response itself.

The answer is to set failWithError in the options passed to authenticate: https://github.com/jaredhanson/passport/issues/126#issuecomment-32333163

From jaredhanson's comment in the issue:

app.post('/login',
  passport.authenticate('local', { failWithError: true }),
  function(req, res, next) {
    // handle success
    if (req.xhr) { return res.json({ id: req.user.id }); }
    return res.redirect('/');
  },
  function(err, req, res, next) {
    // handle error
    if (req.xhr) { return res.json(err); }
    return res.redirect('/login');
  }
);

This will invoke the error handler after Passport calls next(err). For my app, I wrote a generic error handler specific to my use case of just providing a JSON error:

// Middleware error handler for json response
function handleError(err,req,res,next){
    var output = {
        error: {
            name: err.name,
            message: err.message,
            text: err.toString()
        }
    };
    var statusCode = err.status || 500;
    res.status(statusCode).json(output);
}

Then I used it for all api routes:

var api = express.Router();
...
//set up some routes here, attached to api
...
// error handling middleware last
api.use( [
        handleError
        ] );

I didn't find the failWithError option in the documentation. I stumbled upon it while tracing through the code in the debugger.

Also, before I figured this out, I tried the "custom callback" mentioned in the @Kevin_Dente answer, but it didn't work for me. I'm not sure if that was for an older version of Passport or if I was just doing it wrong.

Solution 3

There is an official documentation for Custom Callback:

app.get('/login', function(req, res, next) {
  passport.authenticate('local', function(err, user, info) {
    if (err) { return next(err); }
    if (!user) { return res.redirect('/login'); }
    req.logIn(user, function(err) {
      if (err) { return next(err); }
      return res.redirect('/users/' + user.username);
    });
  })(req, res, next);
});

https://github.com/passport/www.passportjs.org/blob/master/views/docs/authenticate.md

Solution 4

As per the official documentation of Passport you may use custom callback function to handle the case of failed authorization and override the default message.

If you are developing REST API and then you would want to send out pretty JSON response something as below:

{
    "error": {
        "name": "JsonWebTokenError",
        "message": "invalid signature"
    },
    "message": "You are not authorized to access this protected resource",
    "statusCode": 401,
    "data": [],
    "success": false
}

I was using Passport JWT authentication to secure some of my routes and was applied the authMiddleware as below:

app/middlewares/authMiddleware.js

const express = require('express');
const router = express.Router();
const passport = require('passport');
const _ = require('lodash');

router.all('*', function (req, res, next) {
  passport.authenticate('local', function(err, user, info) {

    // If authentication failed, `user` will be set to false. If an exception occurred, `err` will be set.
    if (err || !user || _.isEmpty(user)) {
      // PASS THE ERROR OBJECT TO THE NEXT ROUTE i.e THE APP'S COMMON ERROR HANDLING MIDDLEWARE
      return next(info);
    } else {
      return next();
    }
  })(req, res, next);
});

module.exports = router;

app/routes/approutes.js

const authMiddleware = require('../middlewares/authMiddleware');

module.exports = function (app) {
  // secure the route by applying authentication middleware
  app.use('/users', authMiddleware);
  .....
  ...
  ..

  // ERROR-HANDLING MIDDLEWARE FOR SENDING ERROR RESPONSES TO MAINTAIN A CONSISTENT FORMAT
  app.use((err, req, res, next) => {
    let responseStatusCode = 500;
    let responseObj = {
      success: false,
      data: [],
      error: err,
      message: 'There was some internal server error',
    };

    // IF THERE WAS SOME ERROR THROWN BY PREVIOUS REQUEST
    if (!_.isNil(err)) {
      // IF THE ERROR IS REALTED TO JWT AUTHENTICATE, SET STATUS CODE TO 401 AND SET A CUSTOM MESSAGE FOR UNAUTHORIZED
      if (err.name === 'JsonWebTokenError') {
        responseStatusCode = 401;
        responseObj.message = 'You are not authorized to access this protected resource';
      }
    }

    if (!res.headersSent) {
      res.status(responseStatusCode).json(responseObj);
    }
  });
};

Solution 5

You can do that without custom callbacks using property passReqToCallback in your strategy definition:

passport.use(new LocalStrategy({passReqToCallback: true}, validateUserPassword));

Then you can add your custom auth error code to the request in your strategy code:

var validateUserPassword = function (req, username, password, done) {
    userService.findUser(username)
        .then(user => {
            if (!user) {
                req.authError = "UserNotFound";
                return done(null, false);
            }

And finally you can handle these custom errors in your route:

app.post('/login', passport.authenticate('local', { failWithError: true })      
    function (req, res) {
        ....
    }, function(err, req, res, next) {
        if(req.autherror) {
            res.status(401).send(req.autherror)
        } else {
            ....
        }
    }
);
Share:
57,232
kurisukun
Author by

kurisukun

Updated on July 05, 2022

Comments

  • kurisukun
    kurisukun almost 2 years

    I'm using Node.js as a backend API server for an iPhone client. I'm using Passport.js to authenticate with a local strategy. The relevant code is below:

    // This is in user.js, my user model
    UserSchema.static('authenticate', function(username, password, callback) {
        this.findOne({ username: username }, function(err, user) {
            if (err){
                console.log('findOne error occurred');
                return callback(err);
            }
            if (!user){
                return callback(null, false);
            }
            user.verifyPassword(password, function(err, passwordCorrect){
                if (err){
                    console.log('verifyPassword error occurred');
                    return callback(err);
                }
                if (!passwordCorrect){
                    console.log('Wrong password');
                    return callback(err, false);
                }
                console.log('User Found, returning user');
                return callback(null, user);
            });
        });
    });
    

    and

    // This is in app.js
    app.get('/loginfail', function(req, res){
        res.json(403, {message: 'Invalid username/password'});
    });
    
    app.post('/login',
        passport.authenticate('local', { failureRedirect: '/loginfail', failureFlash: false }),
        function(req, res) {
           res.redirect('/');
    });
    

    Right now, I have managed to redirect a failed login to /loginfail, where I send back some JSON to the iPhone client. However, this doesn't have enough granularity. I want to be able to send back the appropriate errors to the iPhone client, such as: "No user found" or "Password is wrong". With my existing code, I don't see how this can be accomplished.

    I tried to follow the examples for a custom callback on the passport.js site, but I just can't get it to work due to lack of node understanding. How could I modify my code so that I'd be able to send back a res.json with an appropriate error code/message?

    I am trying something like this now:

    // In app.js
    app.post('/login', function(req, res, next) {
        passport.authenticate('local', function(err, user, info) {
            if (err) { return next(err) }
            if (!user) {
                console.log(info);
                // *** Display message without using flash option
                // re-render the login form with a message
                return res.redirect('/login');
            }
            console.log('got user');
            return res.json(200, {user_id: user._id});
        })(req, res, next);
    });
    
    // In user.js
    UserSchema.static('authenticate', function(username, password, callback) {
        this.findOne({ username: username }, function(err, user) {
            if (err){
                console.log('findOne error occurred');
                return callback(err);
            }
            if (!user){
                return callback(null, false);
            }
            user.verifyPassword(password, function(err, passwordCorrect){
                if (err){
                    return callback(err);
                }
                if (!passwordCorrect){
                    return callback(err, false, {message: 'bad password'});
                }
                console.log('User Found, returning user');
                return callback(null, user);
            });
        });
    });
    

    But back when I try to console.log(info), it just says undefined. I don't know how to get this custom callback working...Any help would be appreciated!

  • kurisukun
    kurisukun about 11 years
    Thank you for this. I am aware of the custom callback, but my problem is I just don't understand how to implement it (sorry very new to node and js). I have edited my question with my latest efforts. As you can see, I am trying to use this "info" parameter, but I don't know how to correctly use it -- it is always undefined when I get back from authentication.
  • kurisukun
    kurisukun about 11 years
    Sorry, I just figured out that I needed to add "info" to this as well: passport.use(new LocalStrategy(function(username, password, done) { User.authenticate(username, password, function(err, user, info) { return done(err, user, info); }); } ));
  • jpierson
    jpierson over 8 years
    This conversation helped me come to the same conclusion with a little more substance. github.com/jaredhanson/passport/issues/255
  • Edwin O.
    Edwin O. about 7 years
    still valid in 2017 LOL
  • Muthu
    Muthu almost 7 years
    @kurisukun Why have you passed (req, res, next) in passport.authenticate?
  • Toan Tran
    Toan Tran almost 7 years
    still valid. :P
  • Nic
    Nic almost 6 years
    For the future, could you add a couple of code snippets to clarify? This is somewhat hard to parse without it.
  • AndroConsis
    AndroConsis over 2 years
    still valid in 2021