passport.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. const debug = require('debug')('growi:service:PassportService');
  2. const urljoin = require('url-join');
  3. const passport = require('passport');
  4. const LocalStrategy = require('passport-local').Strategy;
  5. const LdapStrategy = require('passport-ldapauth');
  6. const GoogleStrategy = require('passport-google-auth').Strategy;
  7. const GitHubStrategy = require('passport-github').Strategy;
  8. const TwitterStrategy = require('passport-twitter').Strategy;
  9. const OidcStrategy = require('openid-client').Strategy;
  10. const SamlStrategy = require('passport-saml').Strategy;
  11. const OIDCIssuer = require('openid-client').Issuer;
  12. /**
  13. * the service class of Passport
  14. */
  15. class PassportService {
  16. // see '/lib/form/login.js'
  17. static get USERNAME_FIELD() { return 'loginForm[username]' }
  18. static get PASSWORD_FIELD() { return 'loginForm[password]' }
  19. constructor(crowi) {
  20. this.crowi = crowi;
  21. /**
  22. * the flag whether LocalStrategy is set up successfully
  23. */
  24. this.isLocalStrategySetup = false;
  25. /**
  26. * the flag whether LdapStrategy is set up successfully
  27. */
  28. this.isLdapStrategySetup = false;
  29. /**
  30. * the flag whether GoogleStrategy is set up successfully
  31. */
  32. this.isGoogleStrategySetup = false;
  33. /**
  34. * the flag whether GitHubStrategy is set up successfully
  35. */
  36. this.isGitHubStrategySetup = false;
  37. /**
  38. * the flag whether TwitterStrategy is set up successfully
  39. */
  40. this.isTwitterStrategySetup = false;
  41. /**
  42. * the flag whether OidcStrategy is set up successfully
  43. */
  44. this.isOidcStrategySetup = false;
  45. /**
  46. * the flag whether SamlStrategy is set up successfully
  47. */
  48. this.isSamlStrategySetup = false;
  49. /**
  50. * the flag whether serializer/deserializer are set up successfully
  51. */
  52. this.isSerializerSetup = false;
  53. /**
  54. * the keys of mandatory configs for SAML
  55. */
  56. this.mandatoryConfigKeysForSaml = [
  57. 'security:passport-saml:isEnabled',
  58. 'security:passport-saml:entryPoint',
  59. 'security:passport-saml:issuer',
  60. 'security:passport-saml:cert',
  61. 'security:passport-saml:attrMapId',
  62. 'security:passport-saml:attrMapUsername',
  63. 'security:passport-saml:attrMapMail',
  64. ];
  65. }
  66. /**
  67. * reset LocalStrategy
  68. *
  69. * @memberof PassportService
  70. */
  71. resetLocalStrategy() {
  72. debug('LocalStrategy: reset');
  73. passport.unuse('local');
  74. this.isLocalStrategySetup = false;
  75. }
  76. /**
  77. * setup LocalStrategy
  78. *
  79. * @memberof PassportService
  80. */
  81. setupLocalStrategy() {
  82. // check whether the strategy has already been set up
  83. if (this.isLocalStrategySetup) {
  84. throw new Error('LocalStrategy has already been set up');
  85. }
  86. debug('LocalStrategy: setting up..');
  87. const User = this.crowi.model('User');
  88. passport.use(new LocalStrategy(
  89. {
  90. usernameField: PassportService.USERNAME_FIELD,
  91. passwordField: PassportService.PASSWORD_FIELD,
  92. },
  93. (username, password, done) => {
  94. // find user
  95. User.findUserByUsernameOrEmail(username, password, (err, user) => {
  96. if (err) { return done(err) }
  97. // check existence and password
  98. if (!user || !user.isPasswordValid(password)) {
  99. return done(null, false, { message: 'Incorrect credentials.' });
  100. }
  101. return done(null, user);
  102. });
  103. },
  104. ));
  105. this.isLocalStrategySetup = true;
  106. debug('LocalStrategy: setup is done');
  107. }
  108. /**
  109. * reset LdapStrategy
  110. *
  111. * @memberof PassportService
  112. */
  113. resetLdapStrategy() {
  114. debug('LdapStrategy: reset');
  115. passport.unuse('ldapauth');
  116. this.isLdapStrategySetup = false;
  117. }
  118. /**
  119. * Asynchronous configuration retrieval
  120. *
  121. * @memberof PassportService
  122. */
  123. setupLdapStrategy() {
  124. // check whether the strategy has already been set up
  125. if (this.isLdapStrategySetup) {
  126. throw new Error('LdapStrategy has already been set up');
  127. }
  128. const config = this.crowi.config;
  129. const Config = this.crowi.model('Config');
  130. const isLdapEnabled = Config.isEnabledPassportLdap(config);
  131. // when disabled
  132. if (!isLdapEnabled) {
  133. return;
  134. }
  135. debug('LdapStrategy: setting up..');
  136. passport.use(new LdapStrategy(this.getLdapConfigurationFunc(config, { passReqToCallback: true }),
  137. (req, ldapAccountInfo, done) => {
  138. debug('LDAP authentication has succeeded', ldapAccountInfo);
  139. // store ldapAccountInfo to req
  140. req.ldapAccountInfo = ldapAccountInfo;
  141. done(null, ldapAccountInfo);
  142. }));
  143. this.isLdapStrategySetup = true;
  144. debug('LdapStrategy: setup is done');
  145. }
  146. /**
  147. * return attribute name for mapping to username of Crowi DB
  148. *
  149. * @returns
  150. * @memberof PassportService
  151. */
  152. getLdapAttrNameMappedToUsername() {
  153. const config = this.crowi.config;
  154. return config.crowi['security:passport-ldap:attrMapUsername'] || 'uid';
  155. }
  156. /**
  157. * return attribute name for mapping to name of Crowi DB
  158. *
  159. * @returns
  160. * @memberof PassportService
  161. */
  162. getLdapAttrNameMappedToName() {
  163. const config = this.crowi.config;
  164. return config.crowi['security:passport-ldap:attrMapName'] || '';
  165. }
  166. /**
  167. * return attribute name for mapping to name of Crowi DB
  168. *
  169. * @returns
  170. * @memberof PassportService
  171. */
  172. getLdapAttrNameMappedToMail() {
  173. const config = this.crowi.config;
  174. return config.crowi['security:passport-ldap:attrMapMail'] || 'mail';
  175. }
  176. /**
  177. * CAUTION: this method is capable to use only when `req.body.loginForm` is not null
  178. *
  179. * @param {any} req
  180. * @returns
  181. * @memberof PassportService
  182. */
  183. getLdapAccountIdFromReq(req) {
  184. return req.body.loginForm.username;
  185. }
  186. /**
  187. * Asynchronous configuration retrieval
  188. * @see https://github.com/vesse/passport-ldapauth#asynchronous-configuration-retrieval
  189. *
  190. * @param {object} config
  191. * @param {object} opts
  192. * @returns
  193. * @memberof PassportService
  194. */
  195. getLdapConfigurationFunc(config, opts) {
  196. /* eslint-disable no-multi-spaces */
  197. // get configurations
  198. const isUserBind = config.crowi['security:passport-ldap:isUserBind'];
  199. const serverUrl = config.crowi['security:passport-ldap:serverUrl'];
  200. const bindDN = config.crowi['security:passport-ldap:bindDN'];
  201. const bindCredentials = config.crowi['security:passport-ldap:bindDNPassword'];
  202. const searchFilter = config.crowi['security:passport-ldap:searchFilter'] || '(uid={{username}})';
  203. const groupSearchBase = config.crowi['security:passport-ldap:groupSearchBase'];
  204. const groupSearchFilter = config.crowi['security:passport-ldap:groupSearchFilter'];
  205. const groupDnProperty = config.crowi['security:passport-ldap:groupDnProperty'] || 'uid';
  206. /* eslint-enable no-multi-spaces */
  207. // parse serverUrl
  208. // see: https://regex101.com/r/0tuYBB/1
  209. const match = serverUrl.match(/(ldaps?:\/\/[^/]+)\/(.*)?/);
  210. if (match == null || match.length < 1) {
  211. debug('LdapStrategy: serverUrl is invalid');
  212. return (req, callback) => { callback({ message: 'serverUrl is invalid' }) };
  213. }
  214. const url = match[1];
  215. const searchBase = match[2] || '';
  216. debug(`LdapStrategy: url=${url}`);
  217. debug(`LdapStrategy: searchBase=${searchBase}`);
  218. debug(`LdapStrategy: isUserBind=${isUserBind}`);
  219. if (!isUserBind) {
  220. debug(`LdapStrategy: bindDN=${bindDN}`);
  221. debug(`LdapStrategy: bindCredentials=${bindCredentials}`);
  222. }
  223. debug(`LdapStrategy: searchFilter=${searchFilter}`);
  224. debug(`LdapStrategy: groupSearchBase=${groupSearchBase}`);
  225. debug(`LdapStrategy: groupSearchFilter=${groupSearchFilter}`);
  226. debug(`LdapStrategy: groupDnProperty=${groupDnProperty}`);
  227. return (req, callback) => {
  228. // get credentials from form data
  229. const loginForm = req.body.loginForm;
  230. if (!req.form.isValid) {
  231. return callback({ message: 'Incorrect credentials.' });
  232. }
  233. // user bind
  234. const fixedBindDN = (isUserBind)
  235. ? bindDN.replace(/{{username}}/, loginForm.username)
  236. : bindDN;
  237. const fixedBindCredentials = (isUserBind) ? loginForm.password : bindCredentials;
  238. let serverOpt = {
  239. url,
  240. bindDN: fixedBindDN,
  241. bindCredentials: fixedBindCredentials,
  242. searchBase,
  243. searchFilter,
  244. attrMapUsername: this.getLdapAttrNameMappedToUsername(),
  245. attrMapName: this.getLdapAttrNameMappedToName(),
  246. };
  247. if (groupSearchBase && groupSearchFilter) {
  248. serverOpt = Object.assign(serverOpt, { groupSearchBase, groupSearchFilter, groupDnProperty });
  249. }
  250. process.nextTick(() => {
  251. const mergedOpts = Object.assign({
  252. usernameField: PassportService.USERNAME_FIELD,
  253. passwordField: PassportService.PASSWORD_FIELD,
  254. server: serverOpt,
  255. }, opts);
  256. debug('ldap configuration: ', mergedOpts);
  257. // store configuration to req
  258. req.ldapConfiguration = mergedOpts;
  259. callback(null, mergedOpts);
  260. });
  261. };
  262. }
  263. /**
  264. * Asynchronous configuration retrieval
  265. *
  266. * @memberof PassportService
  267. */
  268. setupGoogleStrategy() {
  269. // check whether the strategy has already been set up
  270. if (this.isGoogleStrategySetup) {
  271. throw new Error('GoogleStrategy has already been set up');
  272. }
  273. const config = this.crowi.config;
  274. const Config = this.crowi.model('Config');
  275. const isGoogleEnabled = Config.isEnabledPassportGoogle(config);
  276. // when disabled
  277. if (!isGoogleEnabled) {
  278. return;
  279. }
  280. debug('GoogleStrategy: setting up..');
  281. passport.use(
  282. new GoogleStrategy(
  283. {
  284. clientId: config.crowi['security:passport-google:clientId'] || process.env.OAUTH_GOOGLE_CLIENT_ID,
  285. clientSecret: config.crowi['security:passport-google:clientSecret'] || process.env.OAUTH_GOOGLE_CLIENT_SECRET,
  286. callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
  287. ? urljoin(this.crowi.configManager.getSiteUrl(), '/passport/google/callback') // auto-generated with v3.2.4 and above
  288. : config.crowi['security:passport-google:callbackUrl'] || process.env.OAUTH_GOOGLE_CALLBACK_URI, // DEPRECATED: backward compatible with v3.2.3 and below
  289. skipUserProfile: false,
  290. },
  291. (accessToken, refreshToken, profile, done) => {
  292. if (profile) {
  293. return done(null, profile);
  294. }
  295. return done(null, false);
  296. },
  297. ),
  298. );
  299. this.isGoogleStrategySetup = true;
  300. debug('GoogleStrategy: setup is done');
  301. }
  302. /**
  303. * reset GoogleStrategy
  304. *
  305. * @memberof PassportService
  306. */
  307. resetGoogleStrategy() {
  308. debug('GoogleStrategy: reset');
  309. passport.unuse('google');
  310. this.isGoogleStrategySetup = false;
  311. }
  312. setupGitHubStrategy() {
  313. // check whether the strategy has already been set up
  314. if (this.isGitHubStrategySetup) {
  315. throw new Error('GitHubStrategy has already been set up');
  316. }
  317. const config = this.crowi.config;
  318. const Config = this.crowi.model('Config');
  319. const isGitHubEnabled = Config.isEnabledPassportGitHub(config);
  320. // when disabled
  321. if (!isGitHubEnabled) {
  322. return;
  323. }
  324. debug('GitHubStrategy: setting up..');
  325. passport.use(
  326. new GitHubStrategy(
  327. {
  328. clientID: config.crowi['security:passport-github:clientId'] || process.env.OAUTH_GITHUB_CLIENT_ID,
  329. clientSecret: config.crowi['security:passport-github:clientSecret'] || process.env.OAUTH_GITHUB_CLIENT_SECRET,
  330. callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
  331. ? urljoin(this.crowi.configManager.getSiteUrl(), '/passport/github/callback') // auto-generated with v3.2.4 and above
  332. : config.crowi['security:passport-github:callbackUrl'] || process.env.OAUTH_GITHUB_CALLBACK_URI, // DEPRECATED: backward compatible with v3.2.3 and below
  333. skipUserProfile: false,
  334. },
  335. (accessToken, refreshToken, profile, done) => {
  336. if (profile) {
  337. return done(null, profile);
  338. }
  339. return done(null, false);
  340. },
  341. ),
  342. );
  343. this.isGitHubStrategySetup = true;
  344. debug('GitHubStrategy: setup is done');
  345. }
  346. /**
  347. * reset GitHubStrategy
  348. *
  349. * @memberof PassportService
  350. */
  351. resetGitHubStrategy() {
  352. debug('GitHubStrategy: reset');
  353. passport.unuse('github');
  354. this.isGitHubStrategySetup = false;
  355. }
  356. setupTwitterStrategy() {
  357. // check whether the strategy has already been set up
  358. if (this.isTwitterStrategySetup) {
  359. throw new Error('TwitterStrategy has already been set up');
  360. }
  361. const config = this.crowi.config;
  362. const Config = this.crowi.model('Config');
  363. const isTwitterEnabled = Config.isEnabledPassportTwitter(config);
  364. // when disabled
  365. if (!isTwitterEnabled) {
  366. return;
  367. }
  368. debug('TwitterStrategy: setting up..');
  369. passport.use(
  370. new TwitterStrategy(
  371. {
  372. consumerKey: config.crowi['security:passport-twitter:consumerKey'] || process.env.OAUTH_TWITTER_CONSUMER_KEY,
  373. consumerSecret: config.crowi['security:passport-twitter:consumerSecret'] || process.env.OAUTH_TWITTER_CONSUMER_SECRET,
  374. callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
  375. ? urljoin(this.crowi.configManager.getSiteUrl(), '/passport/twitter/callback') // auto-generated with v3.2.4 and above
  376. : config.crowi['security:passport-twitter:callbackUrl'] || process.env.OAUTH_TWITTER_CALLBACK_URI, // DEPRECATED: backward compatible with v3.2.3 and below
  377. skipUserProfile: false,
  378. },
  379. (accessToken, refreshToken, profile, done) => {
  380. if (profile) {
  381. return done(null, profile);
  382. }
  383. return done(null, false);
  384. },
  385. ),
  386. );
  387. this.isTwitterStrategySetup = true;
  388. debug('TwitterStrategy: setup is done');
  389. }
  390. /**
  391. * reset TwitterStrategy
  392. *
  393. * @memberof PassportService
  394. */
  395. resetTwitterStrategy() {
  396. debug('TwitterStrategy: reset');
  397. passport.unuse('twitter');
  398. this.isTwitterStrategySetup = false;
  399. }
  400. async setupOidcStrategy() {
  401. // check whether the strategy has already been set up
  402. if (this.isOidcStrategySetup) {
  403. throw new Error('OidcStrategy has already been set up');
  404. }
  405. const config = this.crowi.config;
  406. const configManager = this.crowi.configManager;
  407. const isOidcEnabled = configManager.getConfig('crowi', 'security:passport-oidc:isEnabled');
  408. // when disabled
  409. if (!isOidcEnabled) {
  410. return;
  411. }
  412. debug('OidcStrategy: setting up..');
  413. // setup client
  414. // extend oidc request timeouts
  415. OIDCIssuer.defaultHttpOptions = { timeout: 5000 };
  416. const issuerHost = configManager.getConfig('crowi', 'security:passport-oidc:issuerHost') || process.env.OAUTH_OIDC_ISSUER_HOST;
  417. const clientId = configManager.getConfig('crowi', 'security:passport-oidc:clientId') || process.env.OAUTH_OIDC_CLIENT_ID;
  418. const clientSecret = configManager.getConfig('crowi', 'security:passport-oidc:clientSecret') || process.env.OAUTH_OIDC_CLIENT_SECRET;
  419. const redirectUri = (configManager.getConfig('crowi', 'app:siteUrl') != null)
  420. ? urljoin(this.crowi.configManager.getSiteUrl(), '/passport/oidc/callback')
  421. : config.crowi['security:passport-oidc:callbackUrl'] || process.env.OAUTH_OIDC_CALLBACK_URI; // DEPRECATED: backward compatible with v3.2.3 and below
  422. const oidcIssuer = await OIDCIssuer.discover(issuerHost);
  423. debug('Discovered issuer %s %O', oidcIssuer.issuer, oidcIssuer.metadata);
  424. const client = new oidcIssuer.Client({
  425. client_id: clientId,
  426. client_secret: clientSecret,
  427. redirect_uris: [redirectUri],
  428. response_types: ['code'],
  429. });
  430. passport.use('oidc', new OidcStrategy({
  431. client,
  432. params: { scope: 'openid email profile' },
  433. },
  434. ((tokenset, userinfo, done) => {
  435. if (userinfo) {
  436. return done(null, userinfo);
  437. }
  438. return done(null, false);
  439. })));
  440. this.isOidcStrategySetup = true;
  441. debug('OidcStrategy: setup is done');
  442. }
  443. /**
  444. * reset OidcStrategy
  445. *
  446. * @memberof PassportService
  447. */
  448. resetOidcStrategy() {
  449. debug('OidcStrategy: reset');
  450. passport.unuse('oidc');
  451. this.isOidcStrategySetup = false;
  452. }
  453. setupSamlStrategy() {
  454. // check whether the strategy has already been set up
  455. if (this.isSamlStrategySetup) {
  456. throw new Error('SamlStrategy has already been set up');
  457. }
  458. const configManager = this.crowi.configManager;
  459. const isSamlEnabled = configManager.getConfig('crowi', 'security:passport-saml:isEnabled');
  460. // when disabled
  461. if (!isSamlEnabled) {
  462. return;
  463. }
  464. debug('SamlStrategy: setting up..');
  465. passport.use(
  466. new SamlStrategy(
  467. {
  468. entryPoint: configManager.getConfig('crowi', 'security:passport-saml:entryPoint'),
  469. callbackUrl: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
  470. ? urljoin(this.crowi.configManager.getSiteUrl(), '/passport/saml/callback') // auto-generated with v3.2.4 and above
  471. : configManager.getConfig('crowi', 'security:passport-saml:callbackUrl'), // DEPRECATED: backward compatible with v3.2.3 and below
  472. issuer: configManager.getConfig('crowi', 'security:passport-saml:issuer'),
  473. cert: configManager.getConfig('crowi', 'security:passport-saml:cert'),
  474. },
  475. (profile, done) => {
  476. if (profile) {
  477. return done(null, profile);
  478. }
  479. return done(null, false);
  480. },
  481. ),
  482. );
  483. this.isSamlStrategySetup = true;
  484. debug('SamlStrategy: setup is done');
  485. }
  486. /**
  487. * reset SamlStrategy
  488. *
  489. * @memberof PassportService
  490. */
  491. resetSamlStrategy() {
  492. debug('SamlStrategy: reset');
  493. passport.unuse('saml');
  494. this.isSamlStrategySetup = false;
  495. }
  496. /**
  497. * return the keys of the configs mandatory for SAML whose value are empty.
  498. */
  499. getSamlMissingMandatoryConfigKeys() {
  500. const missingRequireds = [];
  501. for (const key of this.mandatoryConfigKeysForSaml) {
  502. if (this.crowi.configManager.getConfig('crowi', key) === null) {
  503. missingRequireds.push(key);
  504. }
  505. }
  506. return missingRequireds;
  507. }
  508. /**
  509. * setup serializer and deserializer
  510. *
  511. * @memberof PassportService
  512. */
  513. setupSerializer() {
  514. // check whether the serializer/deserializer have already been set up
  515. if (this.isSerializerSetup) {
  516. throw new Error('serializer/deserializer have already been set up');
  517. }
  518. debug('setting up serializer and deserializer');
  519. const User = this.crowi.model('User');
  520. passport.serializeUser((user, done) => {
  521. done(null, user.id);
  522. });
  523. passport.deserializeUser(async(id, done) => {
  524. try {
  525. const user = await User.findById(id).populate(User.IMAGE_POPULATION);
  526. if (user == null) {
  527. throw new Error('user not found');
  528. }
  529. done(null, user);
  530. }
  531. catch (err) {
  532. done(err);
  533. }
  534. });
  535. this.isSerializerSetup = true;
  536. }
  537. isSameUsernameTreatedAsIdenticalUser(providerType) {
  538. const key = `security:passport-${providerType}:isSameUsernameTreatedAsIdenticalUser`;
  539. return this.crowi.configManager.getConfig('crowi', key);
  540. }
  541. isSameEmailTreatedAsIdenticalUser(providerType) {
  542. const key = `security:passport-${providerType}:isSameEmailTreatedAsIdenticalUser`;
  543. return this.crowi.configManager.getConfig('crowi', key);
  544. }
  545. }
  546. module.exports = PassportService;