1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 |
|
8 | var SG = require('strong-globalize');
|
9 | var g = SG();
|
10 |
|
11 | var loopback = require('loopback');
|
12 | var passport = require('passport');
|
13 | var _ = require('underscore');
|
14 |
|
15 | module.exports = PassportConfigurator;
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | function PassportConfigurator(app) {
|
25 | if (!(this instanceof PassportConfigurator)) {
|
26 | return new PassportConfigurator(app);
|
27 | }
|
28 | this.app = app;
|
29 | }
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 | PassportConfigurator.prototype.setupModels = function(options) {
|
40 | options = options || {};
|
41 |
|
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 |
|
71 |
|
72 |
|
73 |
|
74 | PassportConfigurator.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 |
|
82 |
|
83 |
|
84 | passport.serializeUser(function(user, done) {
|
85 | done(null, user.id);
|
86 | });
|
87 |
|
88 | passport.deserializeUser(function(id, done) {
|
89 |
|
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 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 | PassportConfigurator.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 |
|
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 |
|
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 |
|
295 |
|
296 |
|
297 |
|
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 |
|
326 |
|
327 |
|
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 |
|
400 | function(req, iss, sub, profile, jwtClaims, accessToken, refreshToken,
|
401 | params, done) {
|
402 |
|
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 |
|
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 |
|
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 |
|
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 |
|
568 |
|
569 | if (link) {
|
570 | self.app[callbackHTTPMethod](callbackPath, passport.authorize(name, _.defaults({
|
571 | session: session,
|
572 |
|
573 | successRedirect: successRedirect(),
|
574 | failureRedirect: failureRedirect,
|
575 | }, options.authOptions)),
|
576 |
|
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 |
|
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 | };
|