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...
...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:
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!
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With