Skip to content

Instantly share code, notes, and snippets.

@JoshElias
Created September 10, 2015 21:47
Show Gist options
  • Save JoshElias/392cc84d1918bc115d20 to your computer and use it in GitHub Desktop.
Save JoshElias/392cc84d1918bc115d20 to your computer and use it in GitHub Desktop.
var loopback = require('loopback');
var passport = require('passport');
var _ = require('underscore');
module.exports = PassportConfigurator;
/**
* The passport configurator
* @param {Object} app The LoopBack app instance
* @returns {PassportConfigurator}
* @constructor
* @class
*/
function PassportConfigurator(app) {
if (!(this instanceof PassportConfigurator)) {
return new PassportConfigurator(app);
}
this.app = app;
}
/**
* Set up data models for user identity/credential and application credential
* @options {Object} options Options for models
* @property {Model} [userModel] The user model class
* @property {Model} [userCredentialModel] The user credential model class
* @property {Model} [userIdentityModel] The user identity model class
* @end
*/
PassportConfigurator.prototype.setupModels = function(options) {
options = options || {};
// Set up relations
this.userModel = options.userModel || loopback.getModelByType(this.app.models.User);
this.userCredentialModel = options.userCredentialModel ||
loopback.getModelByType(this.app.models.UserCredential);
this.userIdentityModel = options.userIdentityModel ||
loopback.getModelByType(this.app.models.UserIdentity);
if (!this.userModel.relations.identities) {
this.userModel.hasMany(this.userIdentityModel, {as: 'identities'});
} else {
this.userIdentityModel = this.userModel.relations.identities.modelTo;
}
if (!this.userModel.relations.credentials) {
this.userModel.hasMany(this.userCredentialModel, {as: 'credentials'});
} else {
this.userCredentialModel = this.userModel.relations.credentials.modelTo;
}
if (!this.userIdentityModel.relations.user) {
this.userIdentityModel.belongsTo(this.userModel, {as: 'user'});
}
if (!this.userCredentialModel.relations.user) {
this.userCredentialModel.belongsTo(this.userModel, {as: 'user'});
}
};
/**
* Initialize the passport configurator
* @param {Boolean} noSession Set to true if no session is required
* @returns {Passport}
*/
PassportConfigurator.prototype.init = function(noSession) {
var self = this;
self.app.use(passport.initialize());
if (!noSession) {
self.app.use(passport.session());
// Serialization and deserialization is only required if passport session is
// enabled
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
// Look up the user instance by id
self.userModel.findById(id, function(err, user) {
if (err || !user) {
return done(err, user);
}
user.identities(function(err, identities) {
user.profiles = identities;
user.credentials(function(err, accounts) {
user.accounts = accounts;
done(err, user);
});
});
});
});
}
return passport;
}
/**
* Configure a Passport strategy provider.
* @param {String} name The provider name
* @options {Object} General Options Options for the auth provider.
* There are general options that apply to all providers, and provider-specific
* options, as described below.
* @property {Boolean} link Set to true if the provider is for third-party
* account linking.
* @property {Object} module The passport strategy module from require.
* @property {String} authScheme The authentication scheme, such as 'local',
* 'oAuth 2.0'.
* @property {Boolean} [session] Set to true if session is required. Valid
* for any auth scheme.
* @property {String} [authPath] Authentication route.
*
* @options {Object} oAuth2 Options Options for oAuth 2.0.
* @property {String} [clientID] oAuth 2.0 client ID.
* @property {String} [clientSecret] oAuth 2.0 client secret.
* @property {String} [callbackURL] oAuth 2.0 callback URL.
* @property {String} [callbackPath] oAuth 2.0 callback route.
* @property {String} [scope] oAuth 2.0 scopes.
* @property {String} [successRedirect] The redirect route if login succeeds.
* For both oAuth 1 and 2.
* @property {String} [failureRedirect] The redirect route if login fails.
* For both oAuth 1 and 2.
*
* @options {Object} Local Strategy Options Options for local
* strategy.
* @property {String} [usernameField] The field name for username on the form
* for local strategy.
* @property {String} [passwordField] The field name for password on the form
* for local strategy.
*
* @options {Object} oAuth1 Options Options for oAuth 1.0.
* @property {String} [consumerKey] oAuth 1 consumer key.
* @property {String} [consumerSecret] oAuth 1 consumer secret.
* @property {String} [successRedirect] The redirect route if login succeeds.
* For both oAuth 1 and 2.
* @property {String} [failureRedirect] The redirect route if login fails.
* For both oAuth 1 and 2.
*
* @options {Object} OpenID Options Options for OpenID.
* @property {String} [returnURL] OpenID return URL.
* @property {String} [realm] OpenID realm.
* @end
*/
PassportConfigurator.prototype.configureProvider = function(name, options) {
var self = this;
options = options || {};
var link = options.link;
var AuthStrategy = require(options.module)[options.strategy || 'Strategy'];
var authScheme = options.authScheme;
if (!authScheme) {
// Guess the authentication scheme
if (options.consumerKey) {
authScheme = 'oAuth1';
} else if (options.realm) {
authScheme = 'OpenID';
} else if (options.clientID) {
authScheme = 'oAuth 2.0';
} else if (options.usernameField) {
authScheme = 'local';
} else {
authScheme = 'local';
}
}
var clientID = options.clientID;
var clientSecret = options.clientSecret;
var callbackURL = options.callbackURL;
var authPath = options.authPath || ((link ? '/link/' : '/auth/') + name);
var callbackPath = options.callbackPath || ((link ? '/link/' : '/auth/') +
name + '/callback');
var successRedirect = options.successRedirect ||
(link ? '/link/account' : '/auth/account');
var failureRedirect = options.failureRedirect ||
(link ? '/link.html' : '/login.html');
var scope = options.scope;
var authType = authScheme.toLowerCase();
var session = !!options.session;
var loginCallback = options.loginCallback || function(req, done) {
return function(err, user, identity, token) {
var authInfo = {
identity: identity
};
if (token) {
authInfo.accessToken = token;
}
done(err, user, authInfo);
};
};
switch (authType) {
case 'ldap':
passport.use(name, new AuthStrategy(_.defaults({
usernameField: options.usernameField || 'username',
passwordField: options.passwordField || 'password',
session: options.session, authInfo: true,
passReqToCallback: true
}, options),
function(req, user, done) {
if (user) {
var LdapAttributeForUsername = options.LdapAttributeForUsername || 'cn'
var LdapAttributeForMail = options.LdapAttributeForMail || 'mail'
var externalId = user[options.LdapAttributeForLogin || 'uid'];
var email = [].concat(user[LdapAttributeForMail])[0]
var profile = {
username: [].concat(user[LdapAttributeForUsername])[0],
id: externalId
}
if (!!email) {
profile.emails = [{value: email}]
}
var OptionsForCreation = _.defaults({
autoLogin: true
}, options)
self.userIdentityModel.login(name, authScheme, profile, {},
OptionsForCreation, loginCallback(req, done))
}
else {
done(null)
}
}
));
break;
case 'local':
passport.use(name, new AuthStrategy(_.defaults({
usernameField: options.usernameField || 'username',
passwordField: options.passwordField || 'password',
session: options.session, authInfo: true
}, options),
function(username, password, done) {
var query = {
where: {
or: [
{username: username},
{email: username}
]
}
};
self.userModel.findOne(query, function(err, user) {
if (err) {
return done(err);
}
if (user) {
var u = user.toJSON();
delete u.password;
var userProfile = {
provider: 'local',
id: u.id,
username: u.username,
emails: [
{
value: u.email
}
],
status: u.status,
accessToken: null
};
// If we need a token as well, authenticate using Loopbacks
// own login system, else defer to a simple password check
//will grab user info from providers.json file. Right now
//this only can use email and username, which are the 2 most common
if (options.setAccessToken) {
switch (options.usernameField) {
case 'email':
login({email: username, password: password});
break;
case 'username':
login({username: username, password: password});
break;
}
function login(creds) {
self.userModel.login(creds,
function(err, accessToken) {
if (err) {
done(err);
}
if (accessToken) {
userProfile.accessToken = accessToken;
done(null, user, {accessToken: accessToken});
} else {
done(null, false, {message: 'Failed to create token.'});
}
});
}
} else {
user.hasPassword(password, function(err, ok) {
if (ok) {
done(null, userProfile);
} else {
return done(null, false, {message: 'Incorrect password.'});
}
});
}
} else {
return done(null, false, {message: 'Incorrect username.'});
}
});
}
));
break;
case 'oauth':
case 'oauth1':
case 'oauth 1.0':
passport.use(name, new AuthStrategy(_.defaults({
consumerKey: options.consumerKey,
consumerSecret: options.consumerSecret,
callbackURL: callbackURL,
passReqToCallback: true
}, options),
function(req, token, tokenSecret, profile, done) {
if (link) {
if (req.user) {
self.userCredentialModel.link(
req.user.id, name, authScheme, profile,
{token: token, tokenSecret: tokenSecret}, options, done);
} else {
done('No user is logged in');
}
} else {
self.userIdentityModel.login(name, authScheme, profile,
{
token: token,
tokenSecret: tokenSecret
}, options, loginCallback(req, done));
}
}
));
break;
case 'openid':
passport.use(name, new AuthStrategy(_.defaults({
returnURL: options.returnURL,
realm: options.realm,
callbackURL: callbackURL,
passReqToCallback: true
}, options),
function(req, identifier, profile, done) {
if (link) {
if (req.user) {
self.userCredentialModel.link(
req.user.id, name, authScheme, profile,
{identifier: identifier}, options, done);
} else {
done('No user is logged in');
}
} else {
self.userIdentityModel.login(name, authScheme, profile,
{identifier: identifier}, options, loginCallback(req, done));
}
}
));
break;
case 'openid connect':
passport.use(name, new AuthStrategy(_.defaults({
clientID: clientID,
clientSecret: clientSecret,
callbackURL: callbackURL,
passReqToCallback: true
}, options),
function(req, accessToken, refreshToken, profile, done) {
if (link) {
if (req.user) {
self.userCredentialModel.link(
req.user.id, name, authScheme, profile,
{
accessToken: accessToken,
refreshToken: refreshToken
}, options, done);
} else {
done('No user is logged in');
}
} else {
self.userIdentityModel.login(name, authScheme, profile,
{accessToken: accessToken, refreshToken: refreshToken},
options, loginCallback(req, done));
}
}
));
break;
default:
passport.use(name, new AuthStrategy(_.defaults({
clientID: clientID,
clientSecret: clientSecret,
callbackURL: callbackURL,
passReqToCallback: true
}, options),
function(req, accessToken, refreshToken, profile, done) {
if (link) {
if (req.user) {
self.userCredentialModel.link(
req.user.id, name, authScheme, profile,
{
accessToken: accessToken,
refreshToken: refreshToken
}, options, done);
} else {
done('No user is logged in');
}
} else {
self.userIdentityModel.login(name, authScheme, profile,
{accessToken: accessToken, refreshToken: refreshToken},
options, loginCallback(req, done));
}
}
));
}
var defaultCallback = function(req, res, next) {
// The default callback
passport.authenticate(name, _.defaults({session: session},
options.authOptions), function(err, user, info) {
if (err) {
return next(err);
}
if (!user) {
if (!!options.json) {
return res.status(401).json("authentication error")
}
return res.redirect(failureRedirect);
}
if (session) {
req.logIn(user, function(err) {
if (err) {
return next(err);
}
if (info && info.accessToken) {
if (!!options.json) {
return res.json({
'access_token': info.accessToken.id,
userId: user.id
});
} else {
res.cookie('access_token', info.accessToken.id,
{
signed: req.signedCookies ? true : false,
// maxAge is in ms
maxAge: 1000 * info.accessToken.ttl
});
res.cookie('userId', user.id.toString(), {
signed: req.signedCookies ? true : false,
maxAge: 1000 * info.accessToken.ttl
});
}
}
return res.redirect(successRedirect);
});
} else {
if (info && info.accessToken) {
if (!!options.json) {
return res.json({
'access_token': info.accessToken.id,
userId: user.id
});
} else {
res.cookie('access_token', info.accessToken.id, {
signed: req.signedCookies ? true : false,
maxAge: 1000 * info.accessToken.ttl
});
res.cookie('userId', user.id.toString(), {
signed: req.signedCookies ? true : false,
maxAge: 1000 * info.accessToken.ttl
});
}
}
return res.redirect(successRedirect);
}
})(req, res, next);
};
/*
* Redirect the user to Facebook for authentication. When complete,
* Facebook will redirect the user back to the application at
* /auth/facebook/callback with the authorization code
*/
if (authType === 'local') {
self.app.post(authPath, passport.authenticate(
name, options.fn || _.defaults({
successReturnToOrRedirect: options.successReturnToOrRedirect,
successRedirect: options.successRedirect,
failureRedirect: options.failureRedirect,
successFlash: options.successFlash,
failureFlash: options.failureFlash,
scope: scope, session: session
}, options.authOptions)));
} else if (authType === 'ldap') {
var ldapCallback = options.customCallback || defaultCallback;
self.app.post(authPath, ldapCallback)
} else if (link) {
self.app.get(authPath, passport.authorize(name, _.defaults({
scope: scope,
session: session
}, options.authOptions)));
} else {
self.app.get(authPath, passport.authenticate(name, _.defaults({
scope: scope,
session: session
}, options.authOptions)));
}
/*
* Facebook will redirect the user to this URL after approval. Finish the
* authentication process by attempting to obtain an access token using the
* authorization code. If access was granted, the user will be logged in.
* Otherwise, authentication has failed.
*/
if (link) {
self.app.get(callbackPath, passport.authorize(name, _.defaults({
session: session,
// successReturnToOrRedirect: successRedirect,
successRedirect: successRedirect,
failureRedirect: failureRedirect
}, options.authOptions)),
// passport.authorize doesn't handle redirect
function(req, res, next) {
res.redirect(successRedirect);
}, function(err, req, res, next) {
res.redirect(failureRedirect);
});
} else {
var customCallback = options.customCallback || defaultCallback;
// Register the path and the callback.
self.app.get(callbackPath, customCallback);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment