简体   繁体   中英

Node.js Authentication with Passport: How to flash a message if a field is missing?

I am using passport.js and I'd like to flash a message if the fields of my form are empty. But I don't know how to do it since passport doesn't trigger the strategy callback if those are missing. I really want this use case to be more clear, and I don't want to modify passport. I feel like there is a way to do so but I don't know where! I've tried to use the callback of the route ( app.post ) but it doesn't seem to work the way I tried.

Here is the authenticate function prototype:

Strategy.prototype.authenticate = function(req, options) {
  options = options || {};
  var username = lookup(req.body, this._usernameField) || lookup(req.query, this._usernameField);
  var password = lookup(req.body, this._passwordField) || lookup(req.query, this._passwordField);
  // here is my problem
  if (!username || !password) {
    return this.fail({ message: options.badRequestMessage || 'Missing credentials' }, 400);
  }

  var self = this;

  function verified(err, user, info) {
    if (err) { return self.error(err); }
    if (!user) { return self.fail(info); }
    self.success(user, info);
  }

  try {
    if (self._passReqToCallback) {
      this._verify(req, username, password, verified);
    } else {
      this._verify(username, password, verified);
    }
  } catch (ex) {
    return self.error(ex);
  }
};

Here is my strategy:

 passport.use('local-login', new LocalStrategy({
        usernameField : 'email',
        passwordField : 'password',
        passReqToCallback : true 
    },
    function(req, email, password, done) { 
        // ...
        console.log("Hello");
        User.findOne({ 'local.email' :  email }, function(err, user) {
            if (err)
                return done(err);

            // if no user is found, return the message
            if (!user)
                return done(null, false, req.flash('loginMessage', 'Pas d\'utilisateur avec ce login.')); // req.flash is the way to set flashdata using connect-flash

            // if the user is found but the password is wrong
            if (!user.validPassword(password))
                return done(null, false, req.flash('loginMessage', 'Oops! Mauvais password.')); // create the loginMessage and save it to session as flashdata

            // all is well, return successful user
            return done(null, user);
        });

    }));

And finally my route:

app.get('/login', function(req, res) {

    // render the page and pass in any flash data if it exists
    res.render('login', { title: "Connexion", message: req.flash('loginMessage') }); 
});

// process the login form
    app.post('/login', passport.authenticate('local-login', {
        successRedirect : '/profile', // redirect to the secure profile section
        failureRedirect : '/login', // redirect back to the signup page if there is an error
        failureFlash : true // allow flash messages
    }, function(err, user, info) {
         // Was trying this callback, does'nt work, post callback maybe ?
         console.log("Hello");
    }));

You should not call req.flash in your verify callback. Instead you should return a message as shown in the documentation . Passport will put the message returned to flash message when failureFlash: true :

Setting the failureFlash option to true instructs Passport to flash an error message using the message given by the strategy's verify callback , if any.

Your revised verify callback:

passport.use('local-login', new LocalStrategy({...},
  function(email, password, done) { 
    User.findOne({ 'local.email' :  email }, function(err, user) {
      if (err)
        return done(err);
      if (!user)
        return done(null, false, {message: 'Pas d\'utilisateur avec ce login.'});
      if (!user.validPassword(password))
        return done(null, false, {message: 'Oops! Mauvais password.'});
      return done(null, user);
    });
  }));

And routes:

app.get('/login', function(req, res) {
  console.log(req.flash('error'));
  res.send();
});

app.post('/login', passport.authenticate('local-login', {
  successRedirect : '/profile',
  failureRedirect : '/login',
  failureFlash : true
}));

Edit:

Here's a fully working example: https://gist.github.com/vesse/9e23ff1810089bed4426

Edit:

This does not indeed answer the original question which was I am using passport.js and I'd like to flash a message if the fields of my form are empty . passport-local strategy does just execute fail if the form fields are empty, so they should be checked before the authentication middleware and set the flash message outside passport.

It's an old question, but I had trouble finding an answer. Hopefully this helps others.


I think the documentation is a little incomplete when it comes to using connect-flash . They say:

Note: Using flash messages requires a req.flash() function. Express 2.x provided this functionality, however it was removed from Express 3.x. Use of connect-flash middleware is recommended to provide this functionality when using Express 3.x.

Yet, there's no mention of using req.flash in the done() callback. Based on the scotch.io tutorial , you actually should call req.flash() right there in the callback. It works for me.

// In your strategy
...
if (user) {
    return done( null, false, req.flash('loginMessage','Pas d\'utilisateur avec ce login.') );
...

You will need to use passReqToCallback of course. Also be sure failureFlash is set to true . OP is already doing these correctly.

Now you can check the flash message in the route. Note that connect-flash sends an array of messages . That could be OP's problem, if his template is expecting a string.

// In your routes
app.get('/login', function(req, res) {

    // Test flash messages in the console
    console.log( req.flash('loginMessage') ); // This returns an array
    console.log( req.flash('loginMessage')[0] ); // This returns a string

    // render the page and pass in any flash data if it exists
    res.render('login', {
        title: "Connexion",
        message: req.flash('loginMessage')[0] // Don't forget the index! 
    });

});

If there's a chance of having multiple login messages on a page, pass the whole req.flash('loginMessage') array and iterate through it in your template. Below is an example using nunjucks .


Protip:

If you have many routes with flash messages, you can always set them to res.locals in a middleware route. This will not interfere with other locals, like title . Here is my implementation, using bootstrap alerts .

In my strategy:

...
if (!user){
    return done( null, false, req.flash('danger','No account exists for that email.') );
}
...

In my routes.js:

// Set flash messages
router.get('*', function(req,res,next){
    res.locals.successes = req.flash('success');
    res.locals.dangers = req.flash('danger');
    res.locals.warnings = req.flash('warning');
    next();
});

// Login route
app.get('/login', function(req, res) {
    res.render('login', { title: 'Login'}); 
});

In my nunjucks base template:

<!--Messages-->
{% for danger in dangers %}
    <div class='header alert alert-danger alert-dismissible'>
        <strong><i class="fa fa-exclamation-circle"></i> ERROR:</strong> {{ danger | safe }}
        <a href="#" class='close' data-dismiss="alert" aria-label="close"><i class='fa fa-times'></i></a> 
    </div>
{% endfor %}
{% for warning in warnings %}
    <div class='header alert alert-warning alert-dismissible'>
        <strong><i class="fa fa-check-circle"></i> Warning:</strong> {{ warning | safe }}
        <a href="#" class='close' data-dismiss="alert" aria-label="close"><i class='fa fa-times'></i></a> 
    </div>
{% endfor %}
{% for success in successes %}
    <div class='header alert alert-success alert-dismissible'>
        <strong><i class="fa fa-check-circle"></i> Success!</strong> {{ success | safe }}
        <a href="#" class='close' data-dismiss="alert" aria-label="close"><i class='fa fa-times'></i></a> 
    </div>
{% endfor %}

You need to set badRequestMessage and set failureFlash: true .

Like this:

passport.authenticate('login', {
    successRedirect : '/',
    failureRedirect : '/login',
    badRequestMessage : 'Missing username or password.',
    failureFlash: true
})

After months of on and off trying to get failure flash to work, i finally found a solution which doesnt use the failureFlash feature. I basically created a new route and sent the flash message.

app.post('/login',
  passport.authenticate('local', {failureRedirect: "/loginfailed"}),
  function(req, res) {
    if (!req.user.isActive){
      req.flash("success","Your account needs to be verified. Please check your email to verify your account");
      req.logout();
      res.redirect("back")
    }else{
      res.redirect("/");
    }
  });

  //Route to login page if user failed to login. I created this to allow flash messages and not interfere with regular login route
  app.get("/loginfailed", function(req, res){
    if (!req.user){
      req.flash("success", "Username or password is incorrect.");
      res.redirect("/login");
    }
  });

I had the same problem and I solved it.
Your success message and failure message variables have to match with whatever the passport JS is using. So after playing around, I realize that passport JS is using the variable success to display success flash and error to display failure flash.

So first, you can create a super global variable like this in your app.js:

app.use(function(req, res, next) {
    res.locals.error = req.flash("error");
    res.locals.success = req.flash("success");
    next();
});

Then use those variables in your temple. I am using ejs so it looks like this:

<%if(error && error.length > 0){%>
    <div class="alert alert-danger"><%=error%></div>
<%}%>
    <%if(success && success.length > 0){%>
    <div class="alert alert-success"><%=success%></div>
<%}%>

And finally your passport JS code should be like this:

router.post("/login",passport.authenticate("local", {
    successFlash : "Hey, Welcome back",
    successRedirect : "/mountains",
    failureFlash : true,
    failureRedirect :"/login"
    }), function(req, res){
});

My solution

app.js code:

const flash = require('connect-flash');
app.use(flash());
require('./src/config/passport.js')(app);

local.strategy.js code

const passport = require('passport');
const { Strategy } = require('passport-local');
const userModel = require('./../../../models/userModel');

module.exports = function localStrategy() {
passport.use(new Strategy(
    {
        usernameField: "username",
        passwordField: "password"
    }, (username, password, done) => {
        userModel.findOne({ username }, (err, user) => {
            if (err) {
                res.send(err);
            }
            if (user && (user.username == username && user.password == password)) {
                done(null, user, { message: "Success" });
            } else {
                done(null, false, { message: "Invalid credentials!" });
            }
        });
      }
       ));
  }

authController.js code

function signIn(req, res) {
    res.render('signin', {
        nav,
        title: "Sign in",
        message: req.flash()
    });
};

authRouter.js code

authRouter.route('/signin').get(signIn).post(passport.authenticate('local', {
    successRedirect: '/admin',
    failureRedirect: '/auth/signin',
    failureFlash: true
}));

signin.js template code (my view engine is ejs)

<% if (message) { %>
  <p style="color: red;" class="text-center"><%= message.error %></p>
<% } %>

When fields required for authentication are missing, passport.authenticate will not trigger the Strategy callback as OP points out.
This has to be handled inside the custom callback (scroll down page) in the authenticate function by using the info parameter.
In case of the OP's code like so:

app.post('/login', function (req, res, next) { 
    passport.authenticate('local-login',
    {
      successRedirect: '/profile',
      failureRedirect: '/login',
      failureFlash: true,
    },
    function (error, user, info) {
      //This will print: 'Missing credentials'
      console.log(info.message);
      //So then OP could do something like 
      req.flash(info.message);
      //or in my case I didn't use flash and just did 
      if (info)
        res.status(400).send(info.message);
      ...
    })(req, res, next);
  });

I know this question is old but I have stumbled upon this issue myself and I see that there is still no accepted answer. Furthermore I think that all the answers misinterpreted what the OP was actually asking for - a way to access the badRequestMessage .
PassportJS docs are not very helpful either:

If authentication failed, user will be set to false. If an exception occurred, err will be set. An optional info argument will be passed, containing additional details provided by the strategy's verify callback.

What this actually means is that info parameter can be passed as a third parameter from your strategy like so: done(error,user,info) , but fails to mention that this parameter is used by default in case of missing credentials. Overall I think PassportJS docs could do with some overhaul as they lack detail and link to non-existent examples .

This answer has helped me understand that the missing credentials message is passed in the info parameter.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM