2

Ok, so this is a common error with many causes. I am trying to modify an existing Node-Passport-Facebook module to have local images from the desktop uploaded to a users Facebook account after they log in. That is my goal.
This is the code module I am extending
https://github.com/passport/express-4.x-local-example

which in turn is based on https://github.com/jaredhanson/passport-facebook

I never get past console.log('ERROR HERE... with an error of "This authorization code has been used."
What's confusing is that the auth code returned is ALWAYS DIFFERENT! so how could it already have been used when I try and exchange it for an access token?
Can anyone offer some suggestions, and or next steps I might try? My hunch is that there is something about Passport.js that is not implemented properly.
So my question is, how would I modify the code below (based on this passport facebook example) https://github.com/passport/express-4.x-facebook-example/blob/master/server.js
to upload an image after logging in?

var express = require('express');
var passport = require('passport');
var Strategy = require('passport-facebook').Strategy;

var CLIENTSECRET ='<client secret>';

var APPID ='<app id>';


// Configure the Facebook strategy for use by Passport.
//
// OAuth 2.0-based strategies require a `verify` function which receives the
// credential (`accessToken`) for accessing the Facebook API on the user's
// behalf, along with the user's profile.  The function must invoke `cb`
// with a user object, which will be set at `req.user` in route handlers after
// authentication.
passport.use(new Strategy({

    clientID: APPID,
    clientSecret: CLIENTSECRET,
    callbackURL: 'http://localhost:3000/login/facebook/return',
    enableProof: true
    //callbackURL: 'http://localhost:3000/login/facebook/return'
  },
  function(accessToken, refreshToken, profile, cb) {
    // In this example, the user's Facebook profile is supplied as the user
    // record.  In a production-quality application, the Facebook profile should
    // be associated with a user record in the application's database, which
    // allows for account linking and authentication with other identity
    // providers.
    cb(null, profile);
  }));


// Configure Passport authenticated session persistence.
//
// In order to restore authentication state across HTTP requests, Passport needs
// to serialize users into and deserialize users out of the session.  In a
// production-quality application, this would typically be as simple as
// supplying the user ID when serializing, and querying the user record by ID
// from the database when deserializing.  However, due to the fact that this
// example does not have a database, the complete Twitter profile is serialized
// and deserialized.
passport.serializeUser(function(user, cb) {
  cb(null, user);
});

passport.deserializeUser(function(obj, cb) {
    console.log(" ");
    console.log("ASSERT passport.deserializeUser being called");
    console.log(" ");
    cb(null, obj);
});


// Create a new Express application.
var app = express();

// Configure view engine to render EJS templates.
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');

// Use application-level middleware for common functionality, including
// logging, parsing, and session handling.
app.use(require('morgan')('combined'));
app.use(require('cookie-parser')());
app.use(require('body-parser').urlencoded({ extended: true }));
app.use(require('express-session')({ secret: 'keyboard cat', resave: true, saveUninitialized: true }));

// Initialize Passport and restore authentication state, if any, from the
// session.
app.use(passport.initialize());
//app.use(passport.session());


// Define routes.
app.get('/',
  function(req, res) {
    res.render('home', { user: req.user });
  });

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

app.get('/login/facebook',
  passport.authenticate('facebook'));

app.get('/login/facebook/return', 
  passport.authenticate('facebook', { failureRedirect: '/login' }),
  function(req, res) {

    //my code changes start here!!
    var code = req.query.code;

    console.log("1 ASSERT after successful login! code="+code);
    if(req.query.error) {
        // user might have disallowed the app
        return res.send('login-error ' + req.query.error_description);
    } else if(!code) {
        return res.redirect('/');
    }

    var options={
        host:'graph.facebook.com',      
        path:'/oauth/access_token?client_id='+APPID+'&code='+code +'&client_secret='+CLIENTSECRET+'&redirect_uri=http://localhost:3000/login/faceboo k/return'
    }       
    var https=require('https'); 
    https.get(options,function(res){
         res.setEncoding('utf8');       
         res.on('data', function (chunk) {
                console.log('ERROR HERE'+chunk);
        });
    });    


    console.log("2  ASSERT after successful login!")

     //my code changes end here!!

});

app.get('/profile',
  require('connect-ensure-login').ensureLoggedIn(),
  function(req, res){
    res.render('profile', { user: req.user });
  });

app.listen(3000);
leobelizquierdo
  • 1,648
  • 12
  • 20
Bachalo
  • 6,965
  • 27
  • 95
  • 189
  • I would add that the button or prompt to display the Facebook LogIn is a button that reads"Upload image to Facebook" so the user is tacitly granting permission to upload the image to their account when they log in. – Bachalo Mar 10 '16 at 03:46

4 Answers4

1

You don't need to make a request to /oauth/access_token at all (well you do, but passport has already handled it for you). That endpoint is for getting an access token when you don't have one, but you already have an access token here:

passport.use(new Strategy({
    clientID: APPID,
    clientSecret: CLIENTSECRET,
    callbackURL: 'http://localhost:3000/login/facebook/return',
    enableProof: true
    //callbackURL: 'http://localhost:3000/login/facebook/return'
  },
  function(accessToken, refreshToken, profile, cb) {
    // You have the access token here!
    cb(null, profile);
  }));

You'll need to save that accessToken some way, so that you can use it later when you make requests to the Graph API. You'll probably want to save it to the user's session, but you can also use a strategy like this: https://stackoverflow.com/a/24474900/772035


If you want the user to grant permission to publish (which you will need them to do, to be able to post to their feeds) you also need to replace every call to passport.authenticate with:

passport.authenticate('facebook', { scope: ['publish_actions'] } );

So that the posting permission is requested when the user first adds your app. Then you'll be able to use the /user/photos endpoint to upload a photo, passing the accessToken that you saved earlier in the query string.

Community
  • 1
  • 1
Paul
  • 139,544
  • 27
  • 275
  • 264
  • Thanks! I came to the same conclusion after much googling elsewhere(see my comment below). After creating a test user I also successfully upload images to a users account with publish_actions. What is remaining is somehow logging a user out automatically from the server AFTER an image is uploaded. After a user logs in they stay authenticated and the Facebook log in is always bypassed. I assume this authentication is saved in a cookie and or session? Anyways will reward you the bounty but would appreciate feedback on the logout issue. – Bachalo Mar 12 '16 at 14:56
  • Also, if I define the accessToken as a global variable to be used once for image upload, do I need to save it? – Bachalo Mar 12 '16 at 15:08
  • Hi @eco_bach , thank you for the bounty. I don't think you want to actually log them out of Facebook, since that will be quite annoying for them if they are browsing Facebook in another tab. There isn't a cookie being saved, but Facebook is remembering that the user has given your app permissions, so you don't need to send them to Facebook to post again. If you want to send them back, you can disconnect them from your app by sending a HTTP DELETE request to [/me/permissions](https://developers.facebook.com/docs/graph-api/reference/user/permissions#revoking) with the access token that you got. – Paul Mar 12 '16 at 17:05
  • @eco_bach I would recommend changing your routes a bit though so that you have function that you use for two separate routes: `/facebook/login/return` and `/post/photo`. Then when you want to post a photo you use the route `/post/photo` which uses `ensureLoggedIn` to make sure you have an access token and then does the request to `/user/photos`. If the user hasn't authorized your app, they'll get sent to the Facebook login and then back to `/facebook/login/return` which will also post the photo (just like it does now); but if they have already logged in it will just post the photo immediately. – Paul Mar 12 '16 at 17:10
  • @eco_bach Storing the access token in a global is risky. It will work most of the time, but it is susceptible to race conditions, where if two or more users attempt to post a photo at the same time, the global will be overwritten by the second user and then you'll post two photos to their news feed, and none to the first user's. – Paul Mar 12 '16 at 17:12
  • Thanks Paul. Just to clarify this is a single use kiosk. Sending a DELETE doesn't do what I need. I don't want to delete the app, just force a log out so the Facebook Login dialog is seen for each subsequent(new or same as previous) user. – Bachalo Mar 12 '16 at 18:37
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/106115/discussion-between-eco-bach-and-paulpro). – Bachalo Mar 12 '16 at 20:39
  • @eco_bach Okay, I'm around now. – Paul Mar 14 '16 at 17:37
0

You need to encode your query params.

var qs = {
    client_id: APPID,
    redirect_uri: 'http://localhost:3000/login/facebook/return',
    client_secret: CLIENTSECRET,
    code: code,
};
options = {
    host:'graph.facebook.com',
    path:'/oauth/access_token?' + require('querystring').stringify(qs),
};

I think that's your problem. The code itself looks fine apart from that. You'll want the querystring module to parse the results too by the way.

Matt Sergeant
  • 2,762
  • 16
  • 12
  • Thanks. Getting same error. However further googling leads me to believe there is more of a larger logic error in my code. In my Passport.js callback function function(accessToken, refreshToken, profile, cb) { I am getting the access token, so no need for an additional call to get it! However, now getting a 'publish_actions' missing error, which leads to other questions 1- If publish actions are needed to post a photo to a user's timeline, do I need to submit my application for approval in order to enable publish_actions? Wish facebook would make things clearer! – Bachalo Mar 10 '16 at 21:10
  • Yes, you generally need to get your service approved by facebook to get anything to work outside of test accounts. – Matt Sergeant Mar 11 '16 at 01:33
0

I solved this error. Follow the progress. Facebook Log in -> Settings -> Apps -> Logged in with Facebook -> Delete Your apps. After deleting your apps, Try to login with Facebook button.

0

you need to define profileFields instead of enableProof: true

 passport.use(new FacebookStrategy({
    clientID:keys.FacebookAppID,
    clientSecret:keys.FacebookAppSecret,
    callbackURL:'http://localhost:3000/auth/facebook/callback',
    profileFields:['email','name','displayName','photos']

},(accessToken,refreshToken,profile,done)=>{
    console.log(profile);
}));