Question

I am working on a web app which which allows user logins through Facebook using Passport.js. My code is as follows:

/* Passport.js */
var passport = require('passport');
var FacebookStrategy = require('passport-facebook').Strategy;

/* DB */
var User = require('../models/db').User;

exports.passport = passport;

passport.use(new FacebookStrategy(
  {
    clientID: '<ID>',
    clientSecret: '<SECRET>',
    callbackURL: 'http://localhost:4242/auth/facebook/callback'
  },
  function (accessToken, refreshToken, profile, done) {
    console.log(profile.provider);
    User.findOrCreate({ "provider": profile.provider,"id": profile.id },
                      function (err, user) { return done(err, user); });
  }
));

passport.serializeUser(function(user, done) {
  console.log('serialize');
  done(null, user.id);
});

passport.deserializeUser(function(id, done) {
  console.log('deserialize');
  User.findOne({"id": id}, function(err, user) {
    done(err, user);
  });
});

This code works fine on Firefox; my user authenticates through Facebook and then routes successfully. On Chrome, however, I sometimes get the following error:

FacebookTokenError: This authorization code has been used.
at Strategy.parseErrorResponse (/Users/Code/Web/node_modules/passport-facebook/lib/strategy.js:198:12)
at Strategy.OAuth2Strategy._createOAuthError (/Users/Code/Web/node_modules/passport-facebook/node_modules/passport-oauth2/lib/strategy.js:337:16)
at /Users/Code/Web/node_modules/passport-facebook/node_modules/passport-oauth2/lib/strategy.js:173:43
at /Users/Code/Web/node_modules/passport-facebook/node_modules/passport-oauth2/node_modules/oauth/lib/oauth2.js:162:18
at passBackControl (/Users/Code/Web/node_modules/passport-facebook/node_modules/passport-oauth2/node_modules/oauth/lib/oauth2.js:109:9)
at IncomingMessage.<anonymous> (/Users/Code/Web/node_modules/passport-facebook/node_modules/passport-oauth2/node_modules/oauth/lib/oauth2.js:128:7)
at IncomingMessage.EventEmitter.emit (events.js:117:20)
at _stream_readable.js:910:16
at process._tickCallback (node.js:415:13)

My print statements reveal some rather unexpected behavior, as depicted in the pictures below:

The unfinished URL waiting to be submitted...

The unfinished URL waiting to be submitted...

...results in print statements in my terminal. ...results in print statements in my terminal

It seems that Chrome attempts to preload the request to Facebook, causing a race condition resulting in an error if the client presses enter at just the right time, as shown below:

An example of the error

I have confirmed the multiple requests with Wireshark. If I wait long enough between autocompletion and submission of the URL (say, 3 seconds), both requests complete without error. The error only occurs if the two requests send just over a second apart. The error is unique to Chrome, as Firefox only sends one request.

Is there anything I can do here? My app surely cannot be the only one which experiences this error when it comes to something as frequent as Facebook authentication. Can I prevent Chrome from preloading somehow? If not, am I resigned to catching the error and just trying to authenticate again?

Bonus question: I seem to be deserializing multiple times for each request. My very first request will print the following:

facebook
serialize
deserialize

Every subsequent successful request prints

deserialize
deserialize
facebook
serialize
deserialize

while unsuccessful request pairs print

deserialize
deserialize
deserialize
deserialize
/* Error */
facebook
serialize

It looks like each request deserializes twice. I read this bug report suggesting a solution, but express.static does come before passport.session in my middleware stack, so that cannot be my problem.

Thanks!

Was it helpful?

Solution

I would leave this as a comment but I don't have the reputation. But Chrome will only prefetch pages when you're typing something into the URL bar, but why would you or a user manually type in /auth/facebook?

One possible solution would be to make the /auth/facebook route only accept POST requests. That would keep Chrome from being able to trigger the route when it tries to preload.

Another possible solution, and I'm not sure how well this would work, would to require a timestamp in the query string, something like /auth/facebook?_t=1406759507255. And only call passport.authenticate('facebook') when the timestamp is close enough to the current time. But I don't think either of these solutions are necessary simply because no one should be typing in that URL at all.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top