login-passport.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. /* eslint-disable no-use-before-define */
  2. module.exports = function(crowi, app) {
  3. const debug = require('debug')('growi:routes:login-passport');
  4. const logger = require('@alias/logger')('growi:routes:login-passport');
  5. const passport = require('passport');
  6. const ExternalAccount = crowi.model('ExternalAccount');
  7. const passportService = crowi.passportService;
  8. const ApiResponse = require('../util/apiResponse');
  9. /**
  10. * success handler
  11. * @param {*} req
  12. * @param {*} res
  13. */
  14. const loginSuccessHandler = (req, res, user) => {
  15. // update lastLoginAt
  16. user.updateLastLoginAt(new Date(), (err, userData) => {
  17. if (err) {
  18. logger.error(`updateLastLoginAt dumps error: ${err}`);
  19. debug(`updateLastLoginAt dumps error: ${err}`);
  20. }
  21. });
  22. const { redirectTo } = req.session;
  23. // remove session.redirectTo
  24. delete req.session.redirectTo;
  25. return res.safeRedirect(redirectTo);
  26. };
  27. /**
  28. * failure handler
  29. * @param {*} req
  30. * @param {*} res
  31. */
  32. const loginFailureHandler = (req, res, message) => {
  33. req.flash('errorMessage', message || req.t('message.sign_in_failure'));
  34. return res.redirect('/login');
  35. };
  36. /**
  37. * middleware for login failure
  38. * @param {*} req
  39. * @param {*} res
  40. */
  41. const loginFailure = (req, res) => {
  42. return loginFailureHandler(req, res, req.t('message.sign_in_failure'));
  43. };
  44. /**
  45. * return true(valid) or false(invalid)
  46. *
  47. * true ... group filter is not defined or the user has one or more groups
  48. * false ... group filter is defined and the user has any group
  49. *
  50. */
  51. function isValidLdapUserByGroupFilter(user) {
  52. let bool = true;
  53. if (user._groups != null) {
  54. if (user._groups.length === 0) {
  55. bool = false;
  56. }
  57. }
  58. return bool;
  59. }
  60. /**
  61. * middleware that login with LdapStrategy
  62. * @param {*} req
  63. * @param {*} res
  64. * @param {*} next
  65. */
  66. const loginWithLdap = async(req, res, next) => {
  67. if (!passportService.isLdapStrategySetup) {
  68. debug('LdapStrategy has not been set up');
  69. return next();
  70. }
  71. if (!req.form.isValid) {
  72. debug('invalid form');
  73. return res.render('login', {
  74. });
  75. }
  76. const providerId = 'ldap';
  77. const strategyName = 'ldapauth';
  78. let ldapAccountInfo;
  79. try {
  80. ldapAccountInfo = await promisifiedPassportAuthentication(strategyName, req, res);
  81. }
  82. catch (err) {
  83. debug(err.message);
  84. return next();
  85. }
  86. // check groups for LDAP
  87. if (!isValidLdapUserByGroupFilter(ldapAccountInfo)) {
  88. return next();
  89. }
  90. /*
  91. * authentication success
  92. */
  93. // it is guaranteed that username that is input from form can be acquired
  94. // because this processes after authentication
  95. const ldapAccountId = passportService.getLdapAccountIdFromReq(req);
  96. const attrMapUsername = passportService.getLdapAttrNameMappedToUsername();
  97. const attrMapName = passportService.getLdapAttrNameMappedToName();
  98. const attrMapMail = passportService.getLdapAttrNameMappedToMail();
  99. const usernameToBeRegistered = ldapAccountInfo[attrMapUsername];
  100. const nameToBeRegistered = ldapAccountInfo[attrMapName];
  101. const mailToBeRegistered = ldapAccountInfo[attrMapMail];
  102. const userInfo = {
  103. id: ldapAccountId,
  104. username: usernameToBeRegistered,
  105. name: nameToBeRegistered,
  106. email: mailToBeRegistered,
  107. };
  108. const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
  109. if (!externalAccount) {
  110. return next();
  111. }
  112. const user = await externalAccount.getPopulatedUser();
  113. // login
  114. await req.logIn(user, (err) => {
  115. if (err) { debug(err.message); return next() }
  116. return loginSuccessHandler(req, res, user);
  117. });
  118. };
  119. /**
  120. * middleware that test credentials with LdapStrategy
  121. *
  122. * @param {*} req
  123. * @param {*} res
  124. */
  125. const testLdapCredentials = (req, res) => {
  126. if (!passportService.isLdapStrategySetup) {
  127. debug('LdapStrategy has not been set up');
  128. return res.json(ApiResponse.success({
  129. status: 'warning',
  130. message: req.t('message.strategy_has_not_been_set_up', { strategy: 'LdapStrategy' }),
  131. }));
  132. }
  133. passport.authenticate('ldapauth', (err, user, info) => {
  134. if (res.headersSent) { // dirty hack -- 2017.09.25
  135. return; // cz: somehow passport.authenticate called twice when ECONNREFUSED error occurred
  136. }
  137. if (err) { // DB Error
  138. logger.error('LDAP Server Error: ', err);
  139. return res.json(ApiResponse.success({
  140. status: 'warning',
  141. message: 'LDAP Server Error occured.',
  142. err,
  143. }));
  144. }
  145. if (info && info.message) {
  146. return res.json(ApiResponse.success({
  147. status: 'warning',
  148. message: info.message,
  149. ldapConfiguration: req.ldapConfiguration,
  150. ldapAccountInfo: req.ldapAccountInfo,
  151. }));
  152. }
  153. if (user) {
  154. // check groups
  155. if (!isValidLdapUserByGroupFilter(user)) {
  156. return res.json(ApiResponse.success({
  157. status: 'warning',
  158. message: 'This user does not belong to any groups designated by the group search filter.',
  159. ldapConfiguration: req.ldapConfiguration,
  160. ldapAccountInfo: req.ldapAccountInfo,
  161. }));
  162. }
  163. return res.json(ApiResponse.success({
  164. status: 'success',
  165. message: 'Successfully authenticated.',
  166. ldapConfiguration: req.ldapConfiguration,
  167. ldapAccountInfo: req.ldapAccountInfo,
  168. }));
  169. }
  170. })(req, res, () => {});
  171. };
  172. /**
  173. * middleware that login with LocalStrategy
  174. * @param {*} req
  175. * @param {*} res
  176. * @param {*} next
  177. */
  178. const loginWithLocal = (req, res, next) => {
  179. if (!passportService.isLocalStrategySetup) {
  180. debug('LocalStrategy has not been set up');
  181. req.flash('warningMessage', req.t('message.strategy_has_not_been_set_up', { strategy: 'LocalStrategy' }));
  182. return next();
  183. }
  184. if (!req.form.isValid) {
  185. return res.render('login', {
  186. });
  187. }
  188. passport.authenticate('local', (err, user, info) => {
  189. debug('--- authenticate with LocalStrategy ---');
  190. debug('user', user);
  191. debug('info', info);
  192. if (err) { // DB Error
  193. logger.error('Database Server Error: ', err);
  194. req.flash('warningMessage', req.t('message.database_error'));
  195. return next(); // pass and the flash message is displayed when all of authentications are failed.
  196. }
  197. if (!user) { return next() }
  198. req.logIn(user, (err) => {
  199. if (err) { debug(err.message); return next() }
  200. return loginSuccessHandler(req, res, user);
  201. });
  202. })(req, res, next);
  203. };
  204. const loginWithGoogle = function(req, res, next) {
  205. if (!passportService.isGoogleStrategySetup) {
  206. debug('GoogleStrategy has not been set up');
  207. req.flash('warningMessage', req.t('message.strategy_has_not_been_set_up', { strategy: 'GoogleStrategy' }));
  208. return next();
  209. }
  210. passport.authenticate('google', {
  211. scope: ['profile', 'email'],
  212. })(req, res);
  213. };
  214. const loginPassportGoogleCallback = async(req, res, next) => {
  215. const globalLang = crowi.configManager.getConfig('crowi', 'app:globalLang');
  216. const providerId = 'google';
  217. const strategyName = 'google';
  218. let response;
  219. try {
  220. response = await promisifiedPassportAuthentication(strategyName, req, res);
  221. }
  222. catch (err) {
  223. return loginFailureHandler(req, res);
  224. }
  225. let name;
  226. switch (globalLang) {
  227. case 'en_US':
  228. name = `${response.name.givenName} ${response.name.familyName}`;
  229. break;
  230. case 'ja_JP':
  231. name = `${response.name.familyName} ${response.name.givenName}`;
  232. break;
  233. default:
  234. name = `${response.name.givenName} ${response.name.familyName}`;
  235. break;
  236. }
  237. const userInfo = {
  238. id: response.id,
  239. username: response.displayName,
  240. name,
  241. };
  242. // Emails are not empty if it exists
  243. // See https://github.com/passport/express-4.x-facebook-example/blob/dfce5495d0313174a1b5039bab2c2dcda7e0eb61/views/profile.ejs
  244. // Both Facebook and Google use OAuth 2.0, the code is similar
  245. // See https://github.com/jaredhanson/passport-google-oauth2/blob/723e8f3e8e711275f89e0163e2c77cfebae33f25/README.md#examples
  246. if (response.emails != null) {
  247. userInfo.email = response.emails[0].value;
  248. userInfo.username = userInfo.email.slice(0, userInfo.email.indexOf('@'));
  249. }
  250. const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
  251. if (!externalAccount) {
  252. return loginFailureHandler(req, res);
  253. }
  254. const user = await externalAccount.getPopulatedUser();
  255. // login
  256. req.logIn(user, (err) => {
  257. if (err) { debug(err.message); return next() }
  258. return loginSuccessHandler(req, res, user);
  259. });
  260. };
  261. const loginWithGitHub = function(req, res, next) {
  262. if (!passportService.isGitHubStrategySetup) {
  263. debug('GitHubStrategy has not been set up');
  264. req.flash('warningMessage', req.t('message.strategy_has_not_been_set_up', { strategy: 'GitHubStrategy' }));
  265. return next();
  266. }
  267. passport.authenticate('github')(req, res);
  268. };
  269. const loginPassportGitHubCallback = async(req, res, next) => {
  270. const providerId = 'github';
  271. const strategyName = 'github';
  272. let response;
  273. try {
  274. response = await promisifiedPassportAuthentication(strategyName, req, res);
  275. }
  276. catch (err) {
  277. return loginFailureHandler(req, res);
  278. }
  279. const userInfo = {
  280. id: response.id,
  281. username: response.username,
  282. name: response.displayName,
  283. };
  284. const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
  285. if (!externalAccount) {
  286. return loginFailureHandler(req, res);
  287. }
  288. const user = await externalAccount.getPopulatedUser();
  289. // login
  290. req.logIn(user, (err) => {
  291. if (err) { debug(err.message); return next() }
  292. return loginSuccessHandler(req, res, user);
  293. });
  294. };
  295. const loginWithTwitter = function(req, res, next) {
  296. if (!passportService.isTwitterStrategySetup) {
  297. debug('TwitterStrategy has not been set up');
  298. req.flash('warningMessage', req.t('message.strategy_has_not_been_set_up', { strategy: 'TwitterStrategy' }));
  299. return next();
  300. }
  301. passport.authenticate('twitter')(req, res);
  302. };
  303. const loginPassportTwitterCallback = async(req, res, next) => {
  304. const providerId = 'twitter';
  305. const strategyName = 'twitter';
  306. let response;
  307. try {
  308. response = await promisifiedPassportAuthentication(strategyName, req, res);
  309. }
  310. catch (err) {
  311. return loginFailureHandler(req, res);
  312. }
  313. const userInfo = {
  314. id: response.id,
  315. username: response.username,
  316. name: response.displayName,
  317. };
  318. const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
  319. if (!externalAccount) {
  320. return loginFailureHandler(req, res);
  321. }
  322. const user = await externalAccount.getPopulatedUser();
  323. // login
  324. req.logIn(user, (err) => {
  325. if (err) { debug(err.message); return next() }
  326. return loginSuccessHandler(req, res, user);
  327. });
  328. };
  329. const loginWithOidc = function(req, res, next) {
  330. if (!passportService.isOidcStrategySetup) {
  331. debug('OidcStrategy has not been set up');
  332. req.flash('warningMessage', req.t('message.strategy_has_not_been_set_up', { strategy: 'OidcStrategy' }));
  333. return next();
  334. }
  335. passport.authenticate('oidc')(req, res);
  336. };
  337. const loginPassportOidcCallback = async(req, res, next) => {
  338. const providerId = 'oidc';
  339. const strategyName = 'oidc';
  340. const attrMapId = crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapId');
  341. const attrMapUserName = crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapUserName');
  342. const attrMapName = crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapName');
  343. const attrMapMail = crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapMail');
  344. let response;
  345. try {
  346. response = await promisifiedPassportAuthentication(strategyName, req, res);
  347. }
  348. catch (err) {
  349. debug(err);
  350. return loginFailureHandler(req, res);
  351. }
  352. const userInfo = {
  353. id: response[attrMapId],
  354. username: response[attrMapUserName],
  355. name: response[attrMapName],
  356. email: response[attrMapMail],
  357. };
  358. debug('mapping response to userInfo', userInfo, response, attrMapId, attrMapUserName, attrMapMail);
  359. const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
  360. if (!externalAccount) {
  361. return loginFailureHandler(req, res);
  362. }
  363. // login
  364. const user = await externalAccount.getPopulatedUser();
  365. req.logIn(user, (err) => {
  366. if (err) { debug(err.message); return next() }
  367. return loginSuccessHandler(req, res, user);
  368. });
  369. };
  370. const loginWithSaml = function(req, res, next) {
  371. if (!passportService.isSamlStrategySetup) {
  372. debug('SamlStrategy has not been set up');
  373. req.flash('warningMessage', req.t('message.strategy_has_not_been_set_up', { strategy: 'SamlStrategy' }));
  374. return next();
  375. }
  376. passport.authenticate('saml')(req, res);
  377. };
  378. const loginPassportSamlCallback = async(req, res) => {
  379. const providerId = 'saml';
  380. const strategyName = 'saml';
  381. const attrMapId = crowi.configManager.getConfig('crowi', 'security:passport-saml:attrMapId');
  382. const attrMapUsername = crowi.configManager.getConfig('crowi', 'security:passport-saml:attrMapUsername');
  383. const attrMapMail = crowi.configManager.getConfig('crowi', 'security:passport-saml:attrMapMail');
  384. const attrMapFirstName = crowi.configManager.getConfig('crowi', 'security:passport-saml:attrMapFirstName') || 'firstName';
  385. const attrMapLastName = crowi.configManager.getConfig('crowi', 'security:passport-saml:attrMapLastName') || 'lastName';
  386. let response;
  387. try {
  388. response = await promisifiedPassportAuthentication(strategyName, req, res);
  389. }
  390. catch (err) {
  391. return loginFailureHandler(req, res);
  392. }
  393. const userInfo = {
  394. id: response[attrMapId],
  395. username: response[attrMapUsername],
  396. email: response[attrMapMail],
  397. };
  398. // determine name
  399. const firstName = response[attrMapFirstName];
  400. const lastName = response[attrMapLastName];
  401. if (firstName != null || lastName != null) {
  402. userInfo.name = `${response[attrMapFirstName]} ${response[attrMapLastName]}`.trim();
  403. }
  404. const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
  405. if (!externalAccount) {
  406. return loginFailureHandler(req, res);
  407. }
  408. const user = await externalAccount.getPopulatedUser();
  409. // Attribute-based Login Control
  410. if (!crowi.passportService.verifySAMLResponseByABLCRule(response)) {
  411. return loginFailureHandler(req, res, 'Sign in failure due to insufficient privileges.');
  412. }
  413. // login
  414. req.logIn(user, (err) => {
  415. if (err != null) {
  416. logger.error(err);
  417. return loginFailureHandler(req, res);
  418. }
  419. return loginSuccessHandler(req, res, user);
  420. });
  421. };
  422. /**
  423. * middleware that login with BasicStrategy
  424. * @param {*} req
  425. * @param {*} res
  426. * @param {*} next
  427. */
  428. const loginWithBasic = async(req, res, next) => {
  429. if (!passportService.isBasicStrategySetup) {
  430. debug('BasicStrategy has not been set up');
  431. req.flash('warningMessage', req.t('message.strategy_has_not_been_set_up', { strategy: 'Basic' }));
  432. return next();
  433. }
  434. const providerId = 'basic';
  435. const strategyName = 'basic';
  436. let userId;
  437. try {
  438. userId = await promisifiedPassportAuthentication(strategyName, req, res);
  439. }
  440. catch (err) {
  441. return loginFailureHandler(req, res);
  442. }
  443. const userInfo = {
  444. id: userId,
  445. username: userId,
  446. name: userId,
  447. };
  448. const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
  449. if (!externalAccount) {
  450. return loginFailureHandler(req, res);
  451. }
  452. const user = await externalAccount.getPopulatedUser();
  453. await req.logIn(user, (err) => {
  454. if (err) { debug(err.message); return next() }
  455. return loginSuccessHandler(req, res, user);
  456. });
  457. };
  458. const promisifiedPassportAuthentication = (strategyName, req, res) => {
  459. return new Promise((resolve, reject) => {
  460. passport.authenticate(strategyName, (err, response, info) => {
  461. if (res.headersSent) { // dirty hack -- 2017.09.25
  462. return; // cz: somehow passport.authenticate called twice when ECONNREFUSED error occurred
  463. }
  464. logger.debug(`--- authenticate with ${strategyName} strategy ---`);
  465. if (err) {
  466. logger.error(`'${strategyName}' passport authentication error: `, err);
  467. reject(err);
  468. }
  469. logger.debug('response', response);
  470. logger.debug('info', info);
  471. // authentication failure
  472. if (!response) {
  473. reject(response);
  474. }
  475. resolve(response);
  476. })(req, res);
  477. });
  478. };
  479. const getOrCreateUser = async(req, res, userInfo, providerId) => {
  480. // get option
  481. const isSameUsernameTreatedAsIdenticalUser = crowi.passportService.isSameUsernameTreatedAsIdenticalUser(providerId);
  482. const isSameEmailTreatedAsIdenticalUser = crowi.passportService.isSameEmailTreatedAsIdenticalUser(providerId);
  483. try {
  484. // find or register(create) user
  485. const externalAccount = await ExternalAccount.findOrRegister(
  486. providerId,
  487. userInfo.id,
  488. userInfo.username,
  489. userInfo.name,
  490. userInfo.email,
  491. isSameUsernameTreatedAsIdenticalUser,
  492. isSameEmailTreatedAsIdenticalUser,
  493. );
  494. return externalAccount;
  495. }
  496. catch (err) {
  497. /* eslint-disable no-else-return */
  498. if (err.name === 'DuplicatedUsernameException') {
  499. if (isSameEmailTreatedAsIdenticalUser || isSameUsernameTreatedAsIdenticalUser) {
  500. // associate to existing user
  501. debug(`ExternalAccount '${userInfo.username}' will be created and bound to the exisiting User account`);
  502. return ExternalAccount.associate(providerId, userInfo.id, err.user);
  503. }
  504. req.flash('provider-DuplicatedUsernameException', providerId);
  505. return;
  506. }
  507. else if (err.name === 'UserUpperLimitException') {
  508. req.flash('warningMessage', req.t('message.maximum_number_of_users'));
  509. return;
  510. }
  511. /* eslint-enable no-else-return */
  512. }
  513. };
  514. return {
  515. loginFailure,
  516. loginWithLdap,
  517. testLdapCredentials,
  518. loginWithLocal,
  519. loginWithGoogle,
  520. loginWithGitHub,
  521. loginWithTwitter,
  522. loginWithOidc,
  523. loginWithSaml,
  524. loginWithBasic,
  525. loginPassportGoogleCallback,
  526. loginPassportGitHubCallback,
  527. loginPassportTwitterCallback,
  528. loginPassportOidcCallback,
  529. loginPassportSamlCallback,
  530. };
  531. };