5
\$\begingroup\$

First attempt was done here
Second attempt was done here

Huge comment at top

/*
 * Export single function that creates the passportControl object
 * The function has two parameters:
 *      app:            The nodejs/express service object.
 *                      This is used to register the end points
 *                      this authentication object listens too.
 *      register:       An object that abstracts user registrations it
 *                      must support the following methods:
 *                          updateUser({<user object>})
 *                          getSavedUser(localUserId,  function(err, localUser) {})
 *
 *      It also uses an external module for configuration (ie holding all the secrets)
 *      This file is not in source control (but in a key repository nice and safe).
 *      config: Expected fields:
 *                  config.app              The URL of the site.
 *                  config.passport         An object containing the secrets for each service
 *                                          The values will depend on the service and the
 *                                          implementation of passport-<service> module
 *                                          See the passport code for more detail
 *                  Example:
 *
                    {
                        app:    'iPubCrawlMaps.com',
                        short:  'iPCM',
                        passport: {
                            facebook: {
                                clientID:       '<My FaceBook Client>'
                                clientSecret:   '<My FaceBool Secret>'
                            },
                            twitter:  {
                                consumerKey:    '<My Twitter Key>',
                                consumerSecret: '<My Twitter Secret>'
                            },
                            google: {
                                returnURL:      'http://<My Site>/api/auth/callback?type=google',
                                realm:          'http://<My Site>/'
                            },
                            foursquare: {
                                clientID:       '<My FourSquare ID>',
                                clientSecret:   '<My FourSquare Secret>'
                            },
                            linkedin: {
                                consumerKey:    '<My LinkedIn Key>',
                                consumerSecret: '<My LinkedIn Secret>'
                            },
                            github: {
                                clientID:       '<My GitHub ID>',
                                clientSecret:   '<My GitHub Secret>'
                            },
                            meetup: {
                                consumerKey:    '<My Meetup Key>',
                                consumerSecret: '<My Meetup Secret>'
                            },
                            aol: {
                                returnURL:      'http://<My Site>/api/auth/callback?type=aol',
                                realm:          'http://<My Site>/'
                            },
                            yahoo: {
                                returnURL:      'http://<My Site>/api/auth/callback?type=yahoo',
                                realm:          'http://<My Site>/'
                            vimeo: {
                                consumerKey:    '<My Vimeo Key>',
                                consumerSecret: '<My Vimeo Secret>'
                            },
                            instagram: {
                                clientID:       '<My Instagram ID>',
                                clientSecret:   '<My Instagram Secret>'
                            },
                            tumblr: {
                                consumerKey:    '<My Tumbler Key>',
                                consumerSecret: '<My Tumbler Secret>'
                            }
                        }
                    };

 *
 *      The passport control object is supposed to be a wrapper for
 *      nodejs/express/passport authentication.
 *
 *      When the object is created it adds three end points to the server for authentication
 *      /api/auth?type=<AuthenticationType>
 *      /api/auth/callback?type=<AuthenticationType>
 *      /api/authexit
 *
 *          Where AuthenticationType is the service doing the authentication.
 *              Eg Facebook/Twitter/Amazon etc
 *
 *      A fourth end point is added to get display info about the user:
 *      And information about supported authentication services. This allows the
 *      front-end to display the appropriate controls without needing code changes.
 *      /api/userInfo
 *
 *      This end point returns the following json object:
 *      {
 *          logedin:        true if the user is currently logged in; false otherwise.
 *          displayName:    The users display name if logged in, '' otherwise
 *          loginMethods:   A list of services that can be used to login if not logged in.
 *      }
 *
 * This object has two public methods:
 *      checkPassport(req, res)
 *      registerUser(req, res)
 *
 *          req:        http request received from node.
 *          res:        response object we use to reply to the request.
 *
 * These are automatically hooked up to the exposed endpoints.
 * To extend this for any particular service just add the appropriate
 * objects to the array built with buildData()
 *
 */

Global Config I use

// Global object for correctly escaping URL
var querystring = require('querystring');
var config      = require('../config.js');

The call to Register a user

function registerUser(register, provider, providerId, displayName, done) {
    register.updateUser({
        provider:    provider,
        providerId:  providerId,
        displayName: displayName
    },
        function(err, localUser) {
            if (err) {done(err); return; }
            done(null, localUser);
        }
        );
}

The heart. A standard function for registering a login stratergy

function addStandardStratergy(stratergyConfig, name, prittyName, type, typeName, config, loginAction) {

    config.callbackURL  = 'http://' + stratergyConfig.app + '/api/auth/callback?type=' + name;

    var Strategy    = require(type).Strategy;
    stratergyConfig.passport.use(new Strategy(config, loginAction));
    stratergyConfig.result.auth[name]       = stratergyConfig.passport.authenticate(name);
    stratergyConfig.result.callback[name]   = function(req, res, page) {stratergyConfig.passport.authenticate(name, { successRedirect: page, failureRedirect: '/login'})(req, res); };
    stratergyConfig.result.services.push({type: name, classInfo: typeName, display: prittyName});
}

Build the data. Basically calls addStandardStratergy() with custom param for each site.

/*
 * This builds the data object central to 'passportControl'
 * The Key: Is the name of the 'AuthenticationType' the value is the passport object that does the authentication.
 * auth:        Handles the initial authentication request.
 * callback:    Handles the callback from the authentication service
 * services:    A list of social services that can be used for logging in
 */
function buildData(passport, register) {

    // Add more strategies as required here.
    var result, stratergyConfig, useDisplayName, useUserName, buildName, OAuthGetLoginAction, OpenIdLoginAction;
    result = {
        auth: {
            default:    function(req, res) {res.redirect('/login?' + querystring.stringify({reg_error: 'Invalid Authentication Type (attempt)'})); }
        },
        callback: {
            default:    function(req, res) {res.redirect('/login?' + querystring.stringify({reg_error: 'Invalid Authentication Type (callback)'})); }
        },
        services: []
    };

    /*
     * Add a call for each social network you want to use for registration
     */

    stratergyConfig = {
        passport:       passport,
        result:         result,
        app:            config.app
    };
    /*
     * I only use these services to log people in to associate them to an account.
     * Each of the services I use provides varying information. All I want is a display name.
     * But each service provides a different set of attributes. So I use the following functions
     * in association with addStandardStratergy() to pull the "Display Name" from the returned data.
     */
    useDisplayName = function(profile) {return profile.displayName || 'anonymous'; };
    useUserName    = function(profile) {return profile.username || 'ananymous'; };
    buildName      = function(profile) {return profile.name.givenName + ' ' + profile.name.familyName; };


    /*
     * Two major authentication methods are used by these sites:
     *      OAuth
     *      OpenID
     *
     * A method for handling each type of authentication.
     */
    OAuthGetLoginAction = function (name, getDisplayName) {
        return function(accessToken, refreshToken, profile, done) {
            registerUser(register, profile.provider, profile.id, getDisplayName(profile), done);
        };
    };
    OpenIdLoginAction = function OpenIdLoginAction(name) {
        return function(identifier, profile, done) {
            registerUser(register, name, identifier, useDisplayName(profile), done);
        };
    };


    /*
     * Register the different services you want to use here.
     * The authentication information is the 5th paramaeter and pulled from the config info
     * described in the main comment above
     */
    //                                    name          prittyName  type                    typeName        config                      loginAction
    //      Name:           Used internally and for callbacks.
    //      prittyName:     Displayed to the user in any API
    //      typeName:       Used for CSS typing in the display.
    //      config:         Object that holds config information for passport.
    //      loginAction:    Function called on successful login to retrieve specific user info (I just get Display name).

    addStandardStratergy(stratergyConfig, 'facebook',   'Facebook', 'passport-facebook',    'facebook',     config.passport.facebook,   OAuthGetLoginAction('facebook', useDisplayName));
    addStandardStratergy(stratergyConfig, 'twitter',    'Twitter',  'passport-twitter',     'twitter',      config.passport.twitter,    OAuthGetLoginAction('twitter', useDisplayName));
    addStandardStratergy(stratergyConfig, 'google',     'Google+',  'passport-google',      'google-plus',  config.passport.google,     OpenIdLoginAction('google'));
    addStandardStratergy(stratergyConfig, 'foursquare', 'Foursquare','passport-foursquare', 'foursquare',   config.passport.foursquare, OAuthGetLoginAction('foursquare', buildName));
    addStandardStratergy(stratergyConfig, 'linkedin',   'Linkedin', 'passport-linkedin',    'linkedin',     config.passport.linkedin,   OAuthGetLoginAction('linkedin', useDisplayName));
    addStandardStratergy(stratergyConfig, 'github',     'GitHub',   'passport-github',      'github',       config.passport.github,     OAuthGetLoginAction('github', useDisplayName));
    addStandardStratergy(stratergyConfig, 'meetup',     'Meetup',   'passport-meetup',      'meetup',       config.passport.meetup,     OAuthGetLoginAction('meetup', useDisplayName));
    addStandardStratergy(stratergyConfig, 'aol',        'AOL',      'passport-aol',         'aol',          config.passport.aol,        OpenIdLoginAction('aol'));
    addStandardStratergy(stratergyConfig, 'yahoo',      'Yahoo!',   'passport-yahoo',       'yahoo',        config.passport.yahoo,      OpenIdLoginAction('yahoo'));
    addStandardStratergy(stratergyConfig, 'vimeo',      'Vimeo',    'passport-vimeo',       'vimeo',        config.passport.vimeo,      OAuthGetLoginAction('vimeo', useDisplayName));
    addStandardStratergy(stratergyConfig, 'instagram',  'Instagram','passport-instagram',   'instagram',    config.passport.instagram,  OAuthGetLoginAction('instagram', useDisplayName));
    addStandardStratergy(stratergyConfig, 'tumblr',     'Tumblr',   'passport-tumblr',      'tumblr',       config.passport.tumblr,     OAuthGetLoginAction('tumblr', useUserName));

    return result;
}

The object we export back to the applicaiton.

See (second try for usage).

module.exports = function(app, register) {

    // App:         Application object
    // register:    The user registration service
    //              This has been abstracted from the passport authentication code.
    //              I will document this interface separately.


    // Get the passport object we reap
    // Correctly initialize and turn on sessions.
    var passport, passportControl;
    passport = require('passport');
    app.use(passport.initialize());
    app.use(passport.session());

    // Set passport to only save the user ID to the session
    passport.serializeUser(function(localUser, done) {
        done(null, localUser.id);
    });

    // Set passport to retrieve the user object using the
    // saved id (see serializeUser).
    passport.deserializeUser(function(localUserId, done) {
        register.getSavedUser(localUserId,  function(err, localUser) {
            if (err) { done(err); return; }
            done(null, localUser);
        });
    });

    // Create the passport control object
    passportControl =  {
        data:           buildData(passport, register),
        checkPassport: function(req, res) {
            req.session.page  = req.query.page || '/';
            return this.performAction(this.data.auth, req, res);
        },
        registerUser: function(req, res) {
            req.query.page    = req.session.page;
            return this.performAction(this.data.callback, req, res);
        },
        deAuthorize: function(req, res) {
            var page  = req.query.page || '/';
            req.logout();
            res.redirect(page);
        },
        performAction: function (dataItem, req, res) {
            var action, page;
            action  = dataItem[req.query.type];
            page  = req.query.page || '/';
            if (action === null) {
                action = dataItem['default'];
            }
            return action(req, res, page);
        },
        authTypes: function() {
            return this.data.services;
        }
    };
    // The service endpoints
    // This will control all authentication.
    app.get('/api/authexit',        function(req, res) { passportControl.deAuthorize(req, res); });
    app.get('/api/auth',            function(req, res) { passportControl.checkPassport(req, res); });
    app.get('/api/auth/callback',   function(req, res) { passportControl.registerUser(req, res); });
    app.get('/api/userInfo',        function(req, res) {
        res.json({
            logedin:        req.user ? true : false,
            displayName:    req.user ? req.user.displayName : '',
            loginMethods:   req.user ? [] : passportControl.authTypes()
        });
    });

    return passportControl;
};
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

I think you're nailing it.

As a nitpick, the indentation is a bit off in the registerUser function definition.

Another tiny thing in the same function, instead of this:

function(err, localUser) {
    if (err) {done(err); return; }
    done(null, localUser);
}

How about the shorter:

function(err, localUser) {
    err ? done(err) : done(null, localUser);
}

Since the method is not supposed to return anything anyway, this will have the same effect as the original, but it's shorter and perhaps a tiny bit easier to read.

\$\endgroup\$
3
  • \$\begingroup\$ The first time that I see a ternary not used for an assignment and thinking I am okay with that, +1 once I can vote again \$\endgroup\$
    – konijn
    Commented Apr 17, 2014 at 13:48
  • \$\begingroup\$ the entire callback could be replaced by the done function. @konijn usually when you hit that piece of sugar is because your callback is not neccessary ;) \$\endgroup\$
    – laconbass
    Commented May 23, 2014 at 19:34
  • \$\begingroup\$ It depends on what is inside done(), undefined is not null, so just calling done() could have adverse side effects \$\endgroup\$
    – konijn
    Commented May 23, 2014 at 20:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.