4

If I send a login request using the form fields action="/login", method="post", it works just fine. Similar to the code available here or here.

But if instead, if I send the same information using jquery/ajax, then passport just doesn't seem to work. There is no actual login happening via passport, but the ajax call gives a misleading success message.

The below isn't working. Though I get "login success" message, login hasn't really happened.

$('#login_button').click(function () {

    var email = $('#email').val();
    var password = $('#password').val();
    alert ('email/pass:' + email + ", " + password);

    $.ajax({
        type: "POST",
        url: "/login",
        data: { email: email , password: password },
        dataType: 'html'
    })
            .done(function () {
                console.log("http request succeeded");
                alert("login success");
            });
});

I need to use the ajax method, so that I can do something useful on the client side, after successful login. E.g. start socket.io.

Please help. My modified code is available here: my-modified-code

laggingreflex
  • 32,948
  • 35
  • 141
  • 196
Kaya Toast
  • 5,267
  • 8
  • 35
  • 59
  • Not sure but try `data: $.param({ email: email , password: password })` instead of just `data: { email: email , password: password }` – T00rk Sep 24 '14 at 14:56
  • You are not checking to see what the AJAX request returns. It may return an error message that will help you diagnose what is wrong. Right now all you are doing is displaying a message when the request completes, not actually processing what was returned. – Ramsay Smith Sep 24 '14 at 14:58
  • @T00rk I tried it now. Doesn't work. In addition, I've tried various combinations of `data: JSON.stringify({ "username": "bob", "password": "secret"})`, `contentType : 'application/json'`, `dataType: "json"` etc. I suspect the issue may be with `passport.js` – Kaya Toast Sep 24 '14 at 15:06
  • @RamsaySmith I'm checking in Chrome 'Network' and under responses, I'm not finding any errors. I get the status as '302 Moved Temporarily' though. – Kaya Toast Sep 24 '14 at 15:14

3 Answers3

5

I tried your code and Passport-wise it works. I did "Local Signup", "Logout", then "Local Login" and was successfully authenticated but nothing indicated that in the UI.

This is related to that 302 you were talking about - the server replied 302 because you have defined successRedirect : '/profile', and then jQuery followed the redirect and received HTML which it cannot parse because it expects JSON. And since you don't have .fail() callback defined in your $.ajax call you don't see it.

The session is fine though which can be seen by going manually to /profile.

When you login using a regular HTML form the browser will send a single HTTP request and act according to the response (e.g render a HTML page, or perform a redirect if it was 302). The same happens but in different context when you call $.ajax - the AJAX call follows the redirect because it made the request, but the browser does not.

You should use separate routes for AJAX and HTML logins, or use a custom callback and determine what to return based on req.accepts().

The separate routes could be eg.

// AJAX logins to this URL, redirect on client side using
// window.location.href if login succeeds
app.post('/login/ajax', passport.authenticate('local-login'));

// HTTP login form send to this URL
app.post('/login', passport.authenticate('local-login', {
  successRedirect : '/profile',
  failureRedirect : '/login',
  failureFlash : true
}));

Custom callback could be something like this (not tested):

app.post('/login', function(req, res, next) {
  passport.authenticate('local-login', function(err, user, info) {
    switch (req.accepts('html', 'json')) {
      case 'html':
        if (err) { return next(err); }
        if (!user) { return res.redirect('/login'); }
        req.logIn(user, function(err) {
          if (err) { return next(err); }
          return res.redirect('/profile');
        });
        break;
      case 'json':
        if (err)  { return next(err); }
        if (!user) { return res.status(401).send({"ok": false}); }
        req.logIn(user, function(err) {
          if (err) { return res.status(401).send({"ok": false}); }
          return res.send({"ok": true});
        });
        break;
      default:
        res.status(406).send();
    }
  })(req, res, next);    
});
vesse
  • 4,871
  • 26
  • 35
  • Thanks for trying out the code. I understood your answer till the last paragraph. I wasn't expecting AJAX to redirect, but Express routing to redirect (which it does when there is no AJAX, just form). Please elaborate "separate routes for AJAX and HTML logins" ... I tried implementing custom callback in isolation, but didn't work (I read it up, but didn't understand `req.accepts()` ... I have been trying, but I don't get it. – Kaya Toast Sep 26 '14 at 15:22
  • Also, when using AJAX, I notice `GET /profile 200 8ms` in the console (by `morgan('dev')` ); which means it is a successful login and `/profile` is indeed being called. But why does't the redirection materialise on the client side ? It works fine when there is no AJAX. – Kaya Toast Sep 26 '14 at 15:39
  • Updated the answer to hopefully answer the questions. – vesse Sep 26 '14 at 16:51
  • Very very useful. Thanks a bunch! Both approaches work perfectly. Minor change needed for closing quotes - from `req.accepts('html, json')` to `req.accepts('html', 'json')`. One separate question please: Why did you use `next` in `app.post('/login', function(req, res, next) {` ... and when is it a good idea to either use `next` or not use `next` ? – Kaya Toast Sep 27 '14 at 03:26
  • Thanks, updated the `req.accepts` part (it was from Express 3). About `next` - check eg. [this answer](http://stackoverflow.com/a/8711139/3349511) that gives quite nice explanation. – vesse Sep 27 '14 at 15:05
2

Nobody can tell without looking at your server side code, AND which passport strategy you are using. For example,a snippet of the local strategy looks like this:

function Strategy(options, verify) {
  // snip 
  this._usernameField = options.usernameField || 'username';
  this._passwordField = options.passwordField || 'password';
  // snip
}
// snip

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);
  // snip

}

The fields you pass from jQuery need to match up to how you have it configured on the server side, and what that strategy is looking for. In this case if you are using a local strategy without configuring the username and password, it looks for the username and passport fields to be passed in either the req.body or req.query.

So no - email will not work. However you can instantiate the Strategy by passing in the options object a field which overrides the default username e.g.,

{
   username: 'email'
}

I have used this previously to authenticate passport via jQuery:

  var login = function (un, pwd) {

        $.ajax({
            type: "POST",
            dataType: 'json',
            data: { "email": un.val(), "password": pwd.val() },
            url: "/rest/login/?format=json",
            success: function (data) {
                window.location.href = '/somewhereElse'
            }
        });
    }

I suspect your dataType needs to be 'json' not 'html'

akaphenom
  • 6,728
  • 10
  • 59
  • 109
  • Thanks for the response. But I've provided link to my entire code, server and client (click on last line: my-modified-code). Also, in my code if you just revert to submitting by form fields, by uncommenting two lines .. (1) `
    ` and (2) `
    ` in `login.ejs` ... it works perfectly fine. Please clone it and try.
    – Kaya Toast Sep 24 '14 at 15:20
  • I tried `dataType: 'json'`. Isn't working. Also, it is not an issue with email vs. username. When I try the same `jquery/ajax` with username, I run into exactly the same problem, after modifying the code (incorporating ajax call) here: https://github.com/maxdow/passport-local/tree/master/examples/express4 – Kaya Toast Sep 24 '14 at 15:29
  • Try taking a peak at this, it was frustrating for me too: http://stackoverflow.com/questions/21855650/passport-authenticate-callback-is-not-passed-req-and-res/21855930#21855930 – akaphenom Sep 24 '14 at 15:47
0

Perhaps this is a better place than buried in the comments. I experienced something similar months ago, and answered my own question here:

Passport authenticate callback is not passed req and res

essentially I was able to get it to work having the post handled with the signature: req, res, next and inside of that handler you call the passport.authenticate with the desired strategy. The passport.authenticate shouldn't be (or at least in my case wouldn't function as) the handler for the route

server.post(route, function(req, res, next){

    passport.authenticate('local', function(err, user) {

    })

})

I was never able to get the passReqToCallback option to work.

Community
  • 1
  • 1
akaphenom
  • 6,728
  • 10
  • 59
  • 109