passport.js 17 KB

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