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>
addStandardStratergy
;) In all seriousness, this code looks very nice. \$\endgroup\$