Search code examples
javascriptnode.jsfacebookpassport.jspassport-facebook

Can I have multiple facebook strategies for passport?


I want to handle the following passport-facebook related operations differently.

  1. Sign-Up with Facebook
  2. Log-In with Facebook
  3. Connect existing account to Facebook

For 'sign-up' I want to do/check:

  • If the user already exists (based on facebook oauth id), redirect to log-in.
  • If the user already exists (based on email address from facebook profile), prompt the user to sign in with email, then connect their facebook account.
  • Create a new user & sign them in.

For 'log-in' and 'connect account' I want to perform a series of other checks/operations.

I've looked at the passport documentation for facebook and the passport-facebook module and a bunch of related Stack Overflow questions, but am still struggling with how to implement this.

How can I implement different facebook strategies (via passport) with different callbackURLs and options?


Solution

  • After a good deal of digging, this is what I ended up doing:


    Create 3 separate passport strategies.

    Each with different names (facebookSignUp vs facebookLogIn vs facebookConnect) and different callbackURL paths (.../sign-up/clbk vs .../log-in/clbk vs .../connect/clbk).

    // facebook strategy 1 (sign-up with facebook)
    passport.use('facebookSignUp', new FacebookStrategy({
            clientID: FACEBOOOK_APP_ID,
            clientSecret: FACEBOOK_APP_SECRET,
            callbackURL: 'http://website/auth/facebook/sign-up/clbk'
        },
        function(accessToken, refreshToken, profile, clbk) {
            return clbk(profile);
        }
    ));
    
    // facebook strategy 2 (log-in with facebook)
    ...
    
    // facebook strategy 3 (connect facebook to existing account) 
    ...
    

    Create routes for each strategy.

    Initial request route & callback route after facebook authentication. So 2 routes per strategy.

    var member = require('../member');              // member module controller
    
    // sign-up with facebook
    app.route('/auth/facebook/sign-up')
        .get(member.facebookSignUp);                // passport redirect to facebook for authentication
    
    // sign-up with facebook callback
    app.route('/auth/facebook/sign-up/clbk')
        .get(
        member.facebookSignUpClbk,                  // parse facebook profile to create new user & check for existing account -> redirect to log-in route
        member.checkEmail,                          // check if email is already used -> throw error 'please log in with email, then connect facebook in settings'
        member.checkUrl,                            // get unique url
        member.signUp,                              // create new user & sign-in
        member.email.welcome,                       // send welcome email
        member.facebookRedirectDashboard            // redirect to member dashboard
    );
    
    // log-in with facebook
    app.route('/auth/facebook/log-in')
        .get(member.facebookLogIn);                 // passport redirect to facebook for authentication
    
    // log-in with facebook callback
    app.route('/auth/facebook/log-in/clbk')
        .get(
        member.facebookLogInClbk,                   // authenticate user and log-in & check if user exists (fb oauth id or email address) -> throw error 'please sign up with facebook or log in with email'
        member.lastLogin,                           // update user's last login field
        member.facebookRedirectDashboard            // redirect to dashboard
    );
    
    // connect facebook profile to existing account
    ...
    
    // connect facebook profile to existing account clbk
    ...
    

    Where the actual passport authentication is performed in the following files/functions.

    member.facebookSignUp just calls passport.authenticate(), which redirects to facebook.

    // member.facebookSignUp
    exports.facebookSignUp = function(req, res, next) {
        passport.authenticate('facebookSignUp', {          // use the 'facebookSignUp' strategy
            display: null,                                 // null = let facebook decide (or 'page' (default), 'popup', 'touch', etc)
            scope: [
                'public_profile',                          // profile returned by default, but specified here anywhere
                'email',                                   // ask for email address
                'user_location'                            // ask for location
            ]
        })(req, res);
    };
    

    member.facebookSignUpClbk gets executed after facebook authorizes the user and redirects to the callback route. It is where everything else happens.

    // member.facebookSignUpClbk
    exports.facebookSignUpClbk = function(req, res, next) {
    
        // parse profile & plug into req.body for new user creation in later fxn
        function parseProfile(profile) {
            // user doc
            req.body = {};
            // facebook profile data
            req.body.facebook = (profile._json) ? profile._json : {id: profile.id};
            // name
            ...
            // email
            ...
            next();
        }
    
        // check existing users (mongoose/mongodb)
        function checkUser(profile) {
            User.findOne({query}, function(err, userDoc) {
                if (err) {
                    return res.redirect(
                        'http://'+req.headers.host+
                        '?header=Sign-Up Error!'+
                        '&message=We had trouble signing you up with Facebook. Please try again'
                    );
                } else if (userDoc) {
                    // redirect to log-in fxn
                    return res.redirect('http://website/auth/facebook/log-in');
                } else {
                    parseProfile(profile);
                }
            });
        }
    
        // passport authentication
        function passportAuth() {
            passport.authenticate('facebookSignUp', function(profile) {
                if (!profile || !profile.id) {
                    return res.redirect(
                        'http://'+req.headers.host+
                        '?header=Sign-Up Error!'+
                        '&message=We had trouble signing you up with Facebook. Please try again or sign-up via email.'
                    );
                } else {
                    checkUser(profile);
                }
            })(req, res);
        }
    
        // start process
        passportAuth();
    };
    

    member.facebookLogIn, .facebookLogInClbk, .facebookConnect, & .facebookConnectClbk are set up in a similar way, just different logic in the 'Clbk' functions depending on what I'm trying to do.


    Hope this helps!