passport.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. const debug = require('debug')('growi:service:PassportService');
  2. const passport = require('passport');
  3. const LocalStrategy = require('passport-local').Strategy;
  4. const LdapStrategy = require('passport-ldapauth');
  5. const GoogleStrategy = require('passport-google-auth').Strategy;
  6. /**
  7. * the service class of Passport
  8. */
  9. class PassportService {
  10. // see '/lib/form/login.js'
  11. static get USERNAME_FIELD() { return 'loginForm[username]' }
  12. static get PASSWORD_FIELD() { return 'loginForm[password]' }
  13. constructor(crowi) {
  14. this.crowi = crowi;
  15. /**
  16. * the flag whether LocalStrategy is set up successfully
  17. */
  18. this.isLocalStrategySetup = false;
  19. /**
  20. * the flag whether LdapStrategy is set up successfully
  21. */
  22. this.isLdapStrategySetup = false;
  23. /**
  24. * the flag whether LdapStrategy is set up successfully
  25. */
  26. this.isGoogleStrategySetup = false;
  27. /**
  28. * the flag whether serializer/deserializer are set up successfully
  29. */
  30. this.isSerializerSetup = false;
  31. }
  32. /**
  33. * reset LocalStrategy
  34. *
  35. * @memberof PassportService
  36. */
  37. resetLocalStrategy() {
  38. debug('LocalStrategy: reset');
  39. passport.unuse('local');
  40. this.isLocalStrategySetup = false;
  41. }
  42. /**
  43. * setup LocalStrategy
  44. *
  45. * @memberof PassportService
  46. */
  47. setupLocalStrategy() {
  48. // check whether the strategy has already been set up
  49. if (this.isLocalStrategySetup) {
  50. throw new Error('LocalStrategy has already been set up');
  51. }
  52. debug('LocalStrategy: setting up..');
  53. const User = this.crowi.model('User');
  54. passport.use(new LocalStrategy(
  55. {
  56. usernameField: PassportService.USERNAME_FIELD,
  57. passwordField: PassportService.PASSWORD_FIELD,
  58. },
  59. (username, password, done) => {
  60. // find user
  61. User.findUserByUsernameOrEmail(username, password, (err, user) => {
  62. if (err) { return done(err) }
  63. // check existence and password
  64. if (!user || !user.isPasswordValid(password)) {
  65. return done(null, false, { message: 'Incorrect credentials.' });
  66. }
  67. return done(null, user);
  68. });
  69. }
  70. ));
  71. this.isLocalStrategySetup = true;
  72. debug('LocalStrategy: setup is done');
  73. }
  74. /**
  75. * reset LdapStrategy
  76. *
  77. * @memberof PassportService
  78. */
  79. resetLdapStrategy() {
  80. debug('LdapStrategy: reset');
  81. passport.unuse('ldapauth');
  82. this.isLdapStrategySetup = false;
  83. }
  84. /**
  85. * Asynchronous configuration retrieval
  86. *
  87. * @memberof PassportService
  88. */
  89. setupLdapStrategy() {
  90. // check whether the strategy has already been set up
  91. if (this.isLdapStrategySetup) {
  92. throw new Error('LdapStrategy has already been set up');
  93. }
  94. const config = this.crowi.config;
  95. const Config = this.crowi.model('Config');
  96. const isLdapEnabled = Config.isEnabledPassportLdap(config);
  97. // when disabled
  98. if (!isLdapEnabled) {
  99. return;
  100. }
  101. debug('LdapStrategy: setting up..');
  102. passport.use(new LdapStrategy(this.getLdapConfigurationFunc(config, {passReqToCallback: true}),
  103. (req, ldapAccountInfo, done) => {
  104. debug('LDAP authentication has succeeded', ldapAccountInfo);
  105. // store ldapAccountInfo to req
  106. req.ldapAccountInfo = ldapAccountInfo;
  107. done(null, ldapAccountInfo);
  108. }
  109. ));
  110. this.isLdapStrategySetup = true;
  111. debug('LdapStrategy: setup is done');
  112. }
  113. /**
  114. * return attribute name for mapping to username of Crowi DB
  115. *
  116. * @returns
  117. * @memberof PassportService
  118. */
  119. getLdapAttrNameMappedToUsername() {
  120. const config = this.crowi.config;
  121. return config.crowi['security:passport-ldap:attrMapUsername'] || 'uid';
  122. }
  123. /**
  124. * return attribute name for mapping to name of Crowi DB
  125. *
  126. * @returns
  127. * @memberof PassportService
  128. */
  129. getLdapAttrNameMappedToName() {
  130. const config = this.crowi.config;
  131. return config.crowi['security:passport-ldap:attrMapName'] || '';
  132. }
  133. /**
  134. * CAUTION: this method is capable to use only when `req.body.loginForm` is not null
  135. *
  136. * @param {any} req
  137. * @returns
  138. * @memberof PassportService
  139. */
  140. getLdapAccountIdFromReq(req) {
  141. return req.body.loginForm.username;
  142. }
  143. /**
  144. * Asynchronous configuration retrieval
  145. * @see https://github.com/vesse/passport-ldapauth#asynchronous-configuration-retrieval
  146. *
  147. * @param {object} config
  148. * @param {object} opts
  149. * @returns
  150. * @memberof PassportService
  151. */
  152. getLdapConfigurationFunc(config, opts) {
  153. // get configurations
  154. const isUserBind = config.crowi['security:passport-ldap:isUserBind'];
  155. const serverUrl = config.crowi['security:passport-ldap:serverUrl'];
  156. const bindDN = config.crowi['security:passport-ldap:bindDN'];
  157. const bindCredentials = config.crowi['security:passport-ldap:bindDNPassword'];
  158. const searchFilter = config.crowi['security:passport-ldap:searchFilter'] || '(uid={{username}})';
  159. const groupSearchBase = config.crowi['security:passport-ldap:groupSearchBase'];
  160. const groupSearchFilter = config.crowi['security:passport-ldap:groupSearchFilter'];
  161. const groupDnProperty = config.crowi['security:passport-ldap:groupDnProperty'] || 'uid';
  162. // parse serverUrl
  163. // see: https://regex101.com/r/0tuYBB/1
  164. const match = serverUrl.match(/(ldaps?:\/\/[^\/]+)\/(.*)?/);
  165. if (match == null || match.length < 1) {
  166. debug('LdapStrategy: serverUrl is invalid');
  167. return (req, callback) => { callback({ message: 'serverUrl is invalid'}) };
  168. }
  169. const url = match[1];
  170. const searchBase = match[2] || '';
  171. debug(`LdapStrategy: url=${url}`);
  172. debug(`LdapStrategy: searchBase=${searchBase}`);
  173. debug(`LdapStrategy: isUserBind=${isUserBind}`);
  174. if (!isUserBind) {
  175. debug(`LdapStrategy: bindDN=${bindDN}`);
  176. debug(`LdapStrategy: bindCredentials=${bindCredentials}`);
  177. }
  178. debug(`LdapStrategy: searchFilter=${searchFilter}`);
  179. debug(`LdapStrategy: groupSearchBase=${groupSearchBase}`);
  180. debug(`LdapStrategy: groupSearchFilter=${groupSearchFilter}`);
  181. debug(`LdapStrategy: groupDnProperty=${groupDnProperty}`);
  182. return (req, callback) => {
  183. // get credentials from form data
  184. const loginForm = req.body.loginForm;
  185. if (!req.form.isValid) {
  186. return callback({ message: 'Incorrect credentials.' });
  187. }
  188. // user bind
  189. const fixedBindDN = (isUserBind) ?
  190. bindDN.replace(/{{username}}/, loginForm.username):
  191. bindDN;
  192. const fixedBindCredentials = (isUserBind) ? loginForm.password : bindCredentials;
  193. let serverOpt = { url, bindDN: fixedBindDN, bindCredentials: fixedBindCredentials, searchBase, searchFilter };
  194. if (groupSearchBase && groupSearchFilter) {
  195. serverOpt = Object.assign(serverOpt, { groupSearchBase, groupSearchFilter, groupDnProperty });
  196. }
  197. process.nextTick(() => {
  198. const mergedOpts = Object.assign({
  199. usernameField: PassportService.USERNAME_FIELD,
  200. passwordField: PassportService.PASSWORD_FIELD,
  201. server: serverOpt,
  202. }, opts);
  203. debug('ldap configuration: ', mergedOpts);
  204. // store configuration to req
  205. req.ldapConfiguration = mergedOpts;
  206. callback(null, mergedOpts);
  207. });
  208. };
  209. }
  210. /**
  211. * Asynchronous configuration retrieval
  212. *
  213. * @memberof PassportService
  214. */
  215. setupGoogleStrategy() {
  216. // check whether the strategy has already been set up
  217. if (this.isGoogleStrategySetup) {
  218. throw new Error('GoogleStrategy has already been set up');
  219. }
  220. const config = this.crowi.config;
  221. const Config = this.crowi.model('Config');
  222. //this
  223. const isGoogleEnabled = Config.isEnabledPassportGoogle(config);
  224. // when disabled
  225. if (!isGoogleEnabled) {
  226. return;
  227. }
  228. debug('GoogleStrategy: setting up..');
  229. passport.use(new GoogleStrategy({
  230. clientId: config.crowi['security:passport-google:clientId'],
  231. clientSecret: config.crowi['security:passport-google:clientSecret'],
  232. callbackURL: 'http://localhost:3000/passport/google/callback', //change this
  233. skipUserProfile: false,
  234. }, function(accessToken, refreshToken, profile, done) {
  235. if (profile) {
  236. return done(null, profile);
  237. }
  238. else {
  239. return done(null, false);
  240. }
  241. }));
  242. this.isGoogleStrategySetup = true;
  243. debug('GoogleStrategy: setup is done');
  244. }
  245. /**
  246. * reset GoogleStrategy
  247. *
  248. * @memberof PassportService
  249. */
  250. resetGoogleStrategy() {
  251. debug('GoogleStrategy: reset');
  252. passport.unuse('google');
  253. this.isGoogleStrategySetup = false;
  254. }
  255. /**
  256. * setup serializer and deserializer
  257. *
  258. * @memberof PassportService
  259. */
  260. setupSerializer() {
  261. // check whether the serializer/deserializer have already been set up
  262. if (this.isSerializerSetup) {
  263. throw new Error('serializer/deserializer have already been set up');
  264. }
  265. debug('setting up serializer and deserializer');
  266. const User = this.crowi.model('User');
  267. passport.serializeUser(function(user, done) {
  268. done(null, user.id);
  269. });
  270. passport.deserializeUser(function(id, done) {
  271. User.findById(id, function(err, user) {
  272. done(err, user);
  273. });
  274. });
  275. this.isSerializerSetup = true;
  276. }
  277. }
  278. module.exports = PassportService;