11
\$\begingroup\$

First attempt was done here:

My second attempt (now using jslint to make sure the spacing is correct and have incorporate the majority of the feedback from previous attempt).

/*
 * 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:       See https://codereview.stackexchange.com/questions/36940/local-user-registration
 *                      For an example implementation. This is still being worked on.
 *
 *      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:
 *                      passport: {
 *                          facebook: {
 *                              clientID:       'FACEBOOK_CLIENT_ID',
 *                              clientSecret:   'FACEBOOK_CLIENT_SECRET',
 *                          },
 *                          twitter:  {
 *                              consumerKey:    'TWITTER_CONSUMER_KEY',
 *                              consumerSecret: 'TWITTER_CONSUMER_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 object for correctly escaping URL
var querystring = require('querystring');
var config      = require('../config.js');

// Private Method
function addStandardStratergy(passport, register, result, name, prittyName, type, app, config) {

    config.callbackURL  = 'http://' + app + '/api/auth/callback?type=' + name;
    console.log('Callback: ' + config.callbackURL);

    var Strategy    = require(type).Strategy;
    /*jslint unparam: true*/
    passport.use(new Strategy(config,
        function(accessToken, refreshToken, profile, done) {
            register.updateUser({
                provider:    profile.provider,
                providerId:  profile.id,
                displayName: profile.displayName
            },
                function(err, localUser) {
                    if (err) {done(err); return; }
                    done(null, localUser);
                });
        }));
    result.auth[name]       = passport.authenticate(name);
    result.callback[name]   = function(req, res, page) {passport.authenticate(name, { successRedirect: page, failureRedirect: '/login'})(req, res); };
    result.services.push({type: name, display: prittyName});
    /*jslint unparam: false*/
}

/*
 * 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.
    /*jslint unparam: true*/
    var 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: []
    };
    /*jslint unparam: false*/

    /*
     * Add a call for each social network you want to use for registration
     */
    addStandardStratergy(passport, register, result, 'facebook', 'Facebook', 'passport-facebook', config.app, config.passport.facebook);
    addStandardStratergy(passport, register, result, 'twitter', 'Twitter', 'passport-twitter',  config.app, config.passport.twitter);

    return result;
}

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;
};

Usage in a node.js application:

Edit buildData()

Add appropriate calls to addStandardStratergy() (see examples provided)

Call In your code like this.

var express     = require('express');
var ppControl   = require('./PassportControl.js');


var app          = express();
var register     = /* Your object for persisting user info */;
var customsAgent = ppControl(app, register);

Done.

HTML on the client end to display login controls

Note: using Angular to do the work:

<script type="text/javascript">
/*<![[CDATA[*/

function LoginController($scope) {

    $scope.logins   = [];

    $scope.doLogin = function(type) {
        window.location.href    = '/api/auth?type=' + type + '&page=' + encodeURIComponent(document.URL);
    };

    /*jslint unparam: true*/
    /* Notice the call here to get the button info */
    $.get(
        '/api/userInfo',
        '',
        function(data, textStatus, request) {
            if (request.status === 200) {
                $scope.logins   = data.loginMethods;
                $scope.$apply();
            }
        },
        "json"
    );
    /*jslint unparam: false*/
}

/*]]>*/
</script>

<!-- fa           Generic button using icon
     fa-facebook  places facebook icon on button
     fa-twitter   places twitter  con on button 
     fa- ...      etc
-->
<div ng-controller="LoginController">
    <!-- Other parts of control removed -->
    <form method="post" accept-charset="UTF-8">
        <button ng-repeat="login in logins" ng-click="doLogin(login.type)">
            <i class="fa fa-{{login.type}}"></i> | Login with {{login.display}}
         </button>
    </form>
</div>
\$\endgroup\$
  • \$\begingroup\$ addStandardStratergy ;) In all seriousness, this code looks very nice. \$\endgroup\$ – konijn Jan 29 '14 at 1:30
  • \$\begingroup\$ Somehow ended up checking your resume. There is a spelling mistake. Javascrip \$\endgroup\$ – Aseem Bansal May 18 '14 at 18:28
  • \$\begingroup\$ @AseemBansal: Which resume (can you provide a link)? I don't think there is javascript on my resume since I don't do it much. \$\endgroup\$ – Martin York May 18 '14 at 19:03
  • \$\begingroup\$ careers.stackoverflow.com/lokiastar \$\endgroup\$ – Aseem Bansal May 19 '14 at 2:40
4
\$\begingroup\$

I really like this code, I only have a few minor observations:

  • 'http://' <- https should be an option without modifying your wrapper ?
  • (request.status === 200) <- You should provide support for when things go wrong ? Also I think success is only called when request.status is 200
  • The fact that result.auth.default and result.auth.callback are so similar bugs me (DRY), but I could not find a neater way to do it
  • Spelling: logedin -> loggedIn

Other than that, very cool, something I would consider using for my own Node projects.

\$\endgroup\$
  • 2
    \$\begingroup\$ Watch out this weekend. I have an update to this code that supports 12 popular sites out of the box. Facebook/Twitter/Google+/FourSquare/LinkedIn/GitHub/Meetup/AOL/Yahoo/Vimeo/Instagram/Tumbler. (Note I did not write any of the actual code I just add glue to plug the already written code (which seems to be this guy github.com/jaredhanson?tab=repositories) \$\endgroup\$ – Martin York Apr 16 '14 at 17:50
  • \$\begingroup\$ codereview.stackexchange.com/q/47392/507 \$\endgroup\$ – Martin York Apr 16 '14 at 20:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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