login-passport.js 17 KB

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