UNPKG

21.6 kBJavaScriptView Raw
1// Copyright IBM Corp. 2014,2016. All Rights Reserved.
2// Node module: loopback-component-passport
3// This file is licensed under the Artistic License 2.0.
4// License text available at https://opensource.org/licenses/Artistic-2.0
5
6'use strict';
7
8var SG = require('strong-globalize');
9var g = SG();
10
11var loopback = require('loopback');
12var passport = require('passport');
13var _ = require('underscore');
14
15module.exports = PassportConfigurator;
16
17/**
18 * The passport configurator
19 * @param {Object} app The LoopBack app instance
20 * @returns {PassportConfigurator}
21 * @constructor
22 * @class
23 */
24function PassportConfigurator(app) {
25 if (!(this instanceof PassportConfigurator)) {
26 return new PassportConfigurator(app);
27 }
28 this.app = app;
29}
30
31/**
32 * Set up data models for user identity/credential and application credential
33 * @options {Object} options Options for models
34 * @property {Model} [userModel] The user model class
35 * @property {Model} [userCredentialModel] The user credential model class
36 * @property {Model} [userIdentityModel] The user identity model class
37 * @end
38 */
39PassportConfigurator.prototype.setupModels = function(options) {
40 options = options || {};
41 // Set up relations
42 this.userModel = options.userModel || loopback.getModelByType(this.app.models.User);
43 this.userCredentialModel = options.userCredentialModel ||
44 loopback.getModelByType(this.app.models.UserCredential);
45 this.userIdentityModel = options.userIdentityModel ||
46 loopback.getModelByType(this.app.models.UserIdentity);
47
48 if (!this.userModel.relations.identities) {
49 this.userModel.hasMany(this.userIdentityModel, {as: 'identities'});
50 } else {
51 this.userIdentityModel = this.userModel.relations.identities.modelTo;
52 }
53
54 if (!this.userModel.relations.credentials) {
55 this.userModel.hasMany(this.userCredentialModel, {as: 'credentials'});
56 } else {
57 this.userCredentialModel = this.userModel.relations.credentials.modelTo;
58 }
59
60 if (!this.userIdentityModel.relations.user) {
61 this.userIdentityModel.belongsTo(this.userModel, {as: 'user'});
62 }
63
64 if (!this.userCredentialModel.relations.user) {
65 this.userCredentialModel.belongsTo(this.userModel, {as: 'user'});
66 }
67};
68
69/**
70 * Initialize the passport configurator
71 * @param {Boolean} noSession Set to true if no session is required
72 * @returns {Passport}
73 */
74PassportConfigurator.prototype.init = function(noSession) {
75 var self = this;
76 self.app.middleware('session:after', passport.initialize());
77
78 if (!noSession) {
79 self.app.middleware('session:after', passport.session());
80
81 // Serialization and deserialization is only required if passport session is
82 // enabled
83
84 passport.serializeUser(function(user, done) {
85 done(null, user.id);
86 });
87
88 passport.deserializeUser(function(id, done) {
89 // Look up the user instance by id
90 self.userModel.findById(id, function(err, user) {
91 if (err || !user) {
92 return done(err, user);
93 }
94 user.identities(function(err, identities) {
95 user.profiles = identities;
96 user.credentials(function(err, accounts) {
97 user.accounts = accounts;
98 done(err, user);
99 });
100 });
101 });
102 });
103 }
104
105 return passport;
106};
107
108/**
109 * Configure a Passport strategy provider.
110 * @param {String} name The provider name
111 * @options {Object} General Options Options for the auth provider.
112 * There are general options that apply to all providers, and provider-specific
113 * options, as described below.
114 * @property {Boolean} link Set to true if the provider is for third-party
115 * account linking.
116 * @property {Object} module The passport strategy module from require.
117 * @property {String} authScheme The authentication scheme, such as 'local',
118 * 'oAuth 2.0'.
119 * @property {Boolean} [session] Set to true if session is required. Valid
120 * for any auth scheme.
121 * @property {String} [authPath] Authentication route.
122 *
123 * @options {Object} oAuth2 Options Options for oAuth 2.0.
124 * @property {String} [clientID] oAuth 2.0 client ID.
125 * @property {String} [clientSecret] oAuth 2.0 client secret.
126 * @property {String} [callbackURL] oAuth 2.0 callback URL.
127 * @property {String} [callbackPath] oAuth 2.0 callback route.
128 * @property {String} [scope] oAuth 2.0 scopes.
129 * @property {String} [successRedirect] The redirect route if login succeeds.
130 * For both oAuth 1 and 2.
131 * @property {String} [failureRedirect] The redirect route if login fails.
132 * For both oAuth 1 and 2.
133 *
134 * @options {Object} Local Strategy Options Options for local
135 * strategy.
136 * @property {String} [usernameField] The field name for username on the form
137 * for local strategy.
138 * @property {String} [passwordField] The field name for password on the form
139 * for local strategy.
140 *
141 * @options {Object} oAuth1 Options Options for oAuth 1.0.
142 * @property {String} [consumerKey] oAuth 1 consumer key.
143 * @property {String} [consumerSecret] oAuth 1 consumer secret.
144 * @property {String} [successRedirect] The redirect route if login succeeds.
145 * For both oAuth 1 and 2.
146 * @property {String} [failureRedirect] The redirect route if login fails.
147 * For both oAuth 1 and 2.
148 *
149 * @options {Object} OpenID Options Options for OpenID.
150 * @property {String} [returnURL] OpenID return URL.
151 * @property {String} [realm] OpenID realm.
152 * @end
153 */
154PassportConfigurator.prototype.configureProvider = function(name, options) {
155 var self = this;
156 options = options || {};
157 var link = options.link;
158 var AuthStrategy = require(options.module)[options.strategy || 'Strategy'];
159
160 if (!AuthStrategy) {
161 AuthStrategy = require(options.module);
162 }
163
164 var authScheme = options.authScheme;
165 if (!authScheme) {
166 // Guess the authentication scheme
167 if (options.consumerKey) {
168 authScheme = 'oAuth1';
169 } else if (options.realm) {
170 authScheme = 'OpenID';
171 } else if (options.clientID) {
172 authScheme = 'oAuth 2.0';
173 } else if (options.usernameField) {
174 authScheme = 'local';
175 } else {
176 authScheme = 'local';
177 }
178 }
179 var provider = options.provider || name;
180 var clientID = options.clientID;
181 var clientSecret = options.clientSecret;
182 var callbackURL = options.callbackURL;
183 var authPath = options.authPath || ((link ? '/link/' : '/auth/') + name);
184 var callbackPath = options.callbackPath || ((link ? '/link/' : '/auth/') +
185 name + '/callback');
186 var callbackHTTPMethod = options.callbackHTTPMethod !== 'post' ? 'get' : 'post';
187
188 // remember returnTo position, set by ensureLoggedIn
189 var successRedirect = function(req, accessToken) {
190 if (!!req && req.session && req.session.returnTo) {
191 var returnTo = req.session.returnTo;
192 delete req.session.returnTo;
193 return appendAccessToken(returnTo, accessToken);
194 }
195 return appendAccessToken(options.successRedirect, accessToken) ||
196 (link ? '/link/account' : '/auth/account');
197 };
198
199 var appendAccessToken = function(url, accessToken) {
200 if (!accessToken) {
201 return url;
202 }
203 return url + '?access-token=' + accessToken.id + '&user-id=' + accessToken.userId;
204 };
205
206 var failureRedirect = options.failureRedirect ||
207 (link ? '/link.html' : '/login.html');
208 var scope = options.scope;
209 var authType = authScheme.toLowerCase();
210
211 var session = !!options.session;
212
213 var loginCallback = options.loginCallback || function(req, done) {
214 return function(err, user, identity, token) {
215 var authInfo = {
216 identity: identity,
217 };
218 if (token) {
219 authInfo.accessToken = token;
220 }
221 done(err, user, authInfo);
222 };
223 };
224
225 var strategy;
226 switch (authType) {
227 case 'ldap':
228 strategy = new AuthStrategy(_.defaults({
229 usernameField: options.usernameField || 'username',
230 passwordField: options.passwordField || 'password',
231 session: options.session, authInfo: true,
232 passReqToCallback: true,
233 }, options),
234 function(req, user, done) {
235 if (user) {
236 var LdapAttributeForUsername = options.LdapAttributeForUsername || 'cn';
237 var LdapAttributeForMail = options.LdapAttributeForMail || 'mail';
238 var externalId = user[options.LdapAttributeForLogin || 'uid'];
239 var email = [].concat(user[LdapAttributeForMail])[0];
240 var profile = {
241 username: [].concat(user[LdapAttributeForUsername])[0],
242 id: externalId,
243 };
244 if (!!email) {
245 profile.emails = [{value: email}];
246 }
247 var OptionsForCreation = _.defaults({
248 autoLogin: true,
249 }, options);
250 self.userIdentityModel.login(provider, authScheme, profile, {},
251 OptionsForCreation, loginCallback(req, done));
252 } else {
253 done(null);
254 }
255 }
256 );
257 break;
258 case 'local':
259 strategy = new AuthStrategy(_.defaults({
260 usernameField: options.usernameField || 'username',
261 passwordField: options.passwordField || 'password',
262 session: options.session, authInfo: true,
263 }, options),
264 function(username, password, done) {
265 var query = {
266 where: {
267 or: [
268 {username: username},
269 {email: username},
270 ],
271 },
272 };
273 self.userModel.findOne(query, function(err, user) {
274 if (err)
275 return done(err);
276
277 var errorMsg = g.f('Invalid username/password or email has not been verified');
278 if (user) {
279 var u = user.toJSON();
280 delete u.password;
281 var userProfile = {
282 provider: 'local',
283 id: u.id,
284 username: u.username,
285 emails: [
286 {
287 value: u.email,
288 },
289 ],
290 status: u.status,
291 accessToken: null,
292 };
293
294 // If we need a token as well, authenticate using Loopbacks
295 // own login system, else defer to a simple password check
296 //will grab user info from providers.json file. Right now
297 //this only can use email and username, which are the 2 most common
298 var login = function(creds) {
299 self.userModel.login(creds,
300 function(err, accessToken) {
301 if (err) {
302 return err.code === 'LOGIN_FAILED' ?
303 done(null, false, {message: g.f('Failed to create token.')}) :
304 done(err);
305 }
306 if (accessToken && user.emailVerified) {
307 userProfile.accessToken = accessToken;
308 done(null, userProfile, {accessToken: accessToken});
309 } else {
310 done(null, false, {message: g.f('Failed to create token.')});
311 }
312 });
313 };
314 if (options.setAccessToken) {
315 switch (options.usernameField) {
316 case 'email':
317 login({email: username, password: password});
318 break;
319 case 'username':
320 login({username: username, password: password});
321 break;
322 }
323 } else {
324 return user.hasPassword(password, function(err, ok) {
325 // Fail to login if email is not verified or invalid username/password.
326 // Unify error message in order not to give indication about the error source for
327 // security purposes.
328 if (ok && user.emailVerified)
329 return done(null, userProfile);
330
331 done(null, false, {message: errorMsg});
332 });
333 }
334 } else {
335 done(null, false, {message: errorMsg});
336 }
337 });
338 }
339 );
340 break;
341 case 'oauth':
342 case 'oauth1':
343 case 'oauth 1.0':
344 strategy = new AuthStrategy(_.defaults({
345 consumerKey: options.consumerKey,
346 consumerSecret: options.consumerSecret,
347 callbackURL: callbackURL,
348 passReqToCallback: true,
349 }, options),
350 function(req, token, tokenSecret, profile, done) {
351 if (link) {
352 if (req.user) {
353 self.userCredentialModel.link(
354 req.user.id, provider, authScheme, profile,
355 {token: token, tokenSecret: tokenSecret}, options, done);
356 } else {
357 done(g.f('No user is logged in'));
358 }
359 } else {
360 self.userIdentityModel.login(provider, authScheme, profile,
361 {
362 token: token,
363 tokenSecret: tokenSecret,
364 }, options, loginCallback(req, done));
365 }
366 }
367 );
368 break;
369 case 'openid':
370 strategy = new AuthStrategy(_.defaults({
371 returnURL: options.returnURL,
372 realm: options.realm,
373 callbackURL: callbackURL,
374 passReqToCallback: true,
375 }, options),
376 function(req, identifier, profile, done) {
377 if (link) {
378 if (req.user) {
379 self.userCredentialModel.link(
380 req.user.id, provider, authScheme, profile,
381 {identifier: identifier}, options, done);
382 } else {
383 done(g.f('No user is logged in'));
384 }
385 } else {
386 self.userIdentityModel.login(provider, authScheme, profile,
387 {identifier: identifier}, options, loginCallback(req, done));
388 }
389 }
390 );
391 break;
392 case 'openid connect':
393 strategy = new AuthStrategy(_.defaults({
394 clientID: clientID,
395 clientSecret: clientSecret,
396 callbackURL: callbackURL,
397 passReqToCallback: true,
398 }, options),
399 // https://github.com/jaredhanson/passport-openidconnect/blob/master/lib/strategy.js#L220-L244
400 function(req, iss, sub, profile, jwtClaims, accessToken, refreshToken,
401 params, done) {
402 // Azure openid connect profile returns oid
403 profile.id = profile.id || profile.oid;
404 if (link) {
405 if (req.user) {
406 self.userCredentialModel.link(
407 req.user.id, provider, authScheme, profile,
408 {
409 accessToken: accessToken,
410 refreshToken: refreshToken,
411 }, options, done);
412 } else {
413 done(g.f('No user is logged in'));
414 }
415 } else {
416 self.userIdentityModel.login(provider, authScheme, profile,
417 {accessToken: accessToken, refreshToken: refreshToken},
418 options, loginCallback(req, done));
419 }
420 }
421 );
422 break;
423 case 'saml':
424 strategy = new AuthStrategy(_.defaults({
425 passReqToCallback: true,
426 }, options),
427 function(req, profile, done) {
428 if (link) {
429 if (req.user) {
430 self.userCredentialModel.link(req.user.id, name, authScheme,
431 profile, {}, options, done);
432 } else {
433 done('No user is logged in');
434 }
435 } else {
436 self.userIdentityModel.login(name, authScheme, profile, {},
437 options, loginCallback(req, done));
438 }
439 }
440 );
441 break;
442 default:
443 strategy = new AuthStrategy(_.defaults({
444 clientID: clientID,
445 clientSecret: clientSecret,
446 callbackURL: callbackURL,
447 passReqToCallback: true,
448 }, options),
449 function(req, accessToken, refreshToken, profile, done) {
450 if (link) {
451 if (req.user) {
452 self.userCredentialModel.link(
453 req.user.id, provider, authScheme, profile,
454 {
455 accessToken: accessToken,
456 refreshToken: refreshToken,
457 }, options, done);
458 } else {
459 done(g.f('No user is logged in'));
460 }
461 } else {
462 self.userIdentityModel.login(provider, authScheme, profile,
463 {accessToken: accessToken, refreshToken: refreshToken},
464 options, loginCallback(req, done));
465 }
466 }
467 );
468 }
469
470 passport.use(name, strategy);
471
472 var defaultCallback = function(req, res, next) {
473 // The default callback
474 passport.authenticate(name, _.defaults({session: session},
475 options.authOptions), function(err, user, info) {
476 if (err) {
477 return next(err);
478 }
479 if (!user) {
480 if (!!options.json) {
481 return res.status(401).json(g.f('authentication error'));
482 }
483 if (options.failureQueryString && info) {
484 return res.redirect(appendErrorToQueryString(failureRedirect, info));
485 }
486 return res.redirect(failureRedirect);
487 }
488 if (session) {
489 req.logIn(user, function(err) {
490 if (err) {
491 return next(err);
492 }
493 if (info && info.accessToken) {
494 if (!!options.json) {
495 return res.json({
496 'access_token': info.accessToken.id,
497 userId: user.id,
498 });
499 } else {
500 res.cookie('access_token', info.accessToken.id,
501 {
502 signed: req.signedCookies ? true : false,
503 // maxAge is in ms
504 maxAge: 1000 * info.accessToken.ttl,
505 domain: (options.domain) ? options.domain : null,
506 });
507 res.cookie('userId', user.id.toString(), {
508 signed: req.signedCookies ? true : false,
509 maxAge: 1000 * info.accessToken.ttl,
510 domain: (options.domain) ? options.domain : null,
511 });
512 }
513 }
514 return res.redirect(successRedirect(req));
515 });
516 } else {
517 if (info && info.accessToken) {
518 if (!!options.json) {
519 return res.json({
520 'access_token': info.accessToken.id,
521 userId: user.id,
522 });
523 } else {
524 res.cookie('access_token', info.accessToken.id, {
525 signed: req.signedCookies ? true : false,
526 maxAge: 1000 * info.accessToken.ttl,
527 });
528 res.cookie('userId', user.id.toString(), {
529 signed: req.signedCookies ? true : false,
530 maxAge: 1000 * info.accessToken.ttl,
531 });
532 }
533 }
534 return res.redirect(successRedirect(req, info.accessToken));
535 }
536 })(req, res, next);
537 };
538 /*
539 * Setup the authentication request URLs.
540 */
541 if (authType === 'local') {
542 self.app.post(authPath, passport.authenticate(
543 name, options.fn || _.defaults({
544 successReturnToOrRedirect: options.successReturnToOrRedirect,
545 successRedirect: options.successRedirect,
546 failureRedirect: options.failureRedirect,
547 successFlash: options.successFlash,
548 failureFlash: options.failureFlash,
549 scope: scope, session: session,
550 }, options.authOptions)));
551 } else if (authType === 'ldap') {
552 var ldapCallback = options.customCallback || defaultCallback;
553 self.app.post(authPath, ldapCallback);
554 } else if (link) {
555 self.app.get(authPath, passport.authorize(name, _.defaults({
556 scope: scope,
557 session: session,
558 }, options.authOptions)));
559 } else {
560 self.app.get(authPath, passport.authenticate(name, _.defaults({
561 scope: scope,
562 session: session,
563 }, options.authOptions)));
564 }
565
566 /*
567 * Setup the authentication callback URLs.
568 */
569 if (link) {
570 self.app[callbackHTTPMethod](callbackPath, passport.authorize(name, _.defaults({
571 session: session,
572 // successReturnToOrRedirect: successRedirect,
573 successRedirect: successRedirect(),
574 failureRedirect: failureRedirect,
575 }, options.authOptions)),
576 // passport.authorize doesn't handle redirect
577 function(req, res, next) {
578 res.redirect(successRedirect(req));
579 }, function(err, req, res, next) {
580 if (options.failureFlash) {
581 if (typeof req.flash !== 'function') {
582 next(new TypeError(g.f('{{req.flash}} is not a function')));
583 }
584 var flash = options.failureFlash;
585 if (typeof flash === 'string') {
586 flash = {type: 'error', message: flash};
587 }
588
589 var type = flash.type || 'error';
590 var msg = flash.message || err.message;
591 if (typeof msg === 'string') {
592 req.flash(type, msg);
593 }
594 }
595
596 if (options.failureQueryString) {
597 return res.redirect(appendErrorToQueryString(failureRedirect, err));
598 }
599
600 res.redirect(failureRedirect);
601 });
602 } else {
603 var customCallback = options.customCallback || defaultCallback;
604 // Register the path and the callback.
605 self.app[callbackHTTPMethod](callbackPath, customCallback);
606 }
607
608 function appendErrorToQueryString(url, err) {
609 var hasQueryString = (url.indexOf('?') !== -1);
610 var separator = (hasQueryString) ? '&' : '?';
611 var fieldValuePair = 'error=' + encodeURIComponent(err);
612 var queryString = url + separator + fieldValuePair;
613 return queryString;
614 }
615
616 return strategy;
617};