You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

309 lines
8.7 KiB

5 years ago
5 years ago
  1. const passport = require('passport')
  2. const passportJWT = require('passport-jwt')
  3. const _ = require('lodash')
  4. const jwt = require('jsonwebtoken')
  5. const moment = require('moment')
  6. const Promise = require('bluebird')
  7. const crypto = Promise.promisifyAll(require('crypto'))
  8. const pem2jwk = require('pem-jwk').pem2jwk
  9. const securityHelper = require('../helpers/security')
  10. /* global WIKI */
  11. module.exports = {
  12. strategies: {},
  13. guest: {
  14. cacheExpiration: moment.utc().subtract(1, 'd')
  15. },
  16. groups: {},
  17. /**
  18. * Initialize the authentication module
  19. */
  20. init() {
  21. this.passport = passport
  22. passport.serializeUser((user, done) => {
  23. done(null, user.id)
  24. })
  25. passport.deserializeUser(async (id, done) => {
  26. try {
  27. const user = await WIKI.models.users.query().findById(id).modifyEager('groups', builder => {
  28. builder.select('groups.id', 'permissions')
  29. })
  30. if (user) {
  31. done(null, user)
  32. } else {
  33. done(new Error(WIKI.lang.t('auth:errors:usernotfound')), null)
  34. }
  35. } catch (err) {
  36. done(err, null)
  37. }
  38. })
  39. this.reloadGroups()
  40. return this
  41. },
  42. /**
  43. * Load authentication strategies
  44. */
  45. async activateStrategies() {
  46. try {
  47. // Unload any active strategies
  48. WIKI.auth.strategies = {}
  49. const currentStrategies = _.keys(passport._strategies)
  50. _.pull(currentStrategies, 'session')
  51. _.forEach(currentStrategies, stg => { passport.unuse(stg) })
  52. // Load JWT
  53. passport.use('jwt', new passportJWT.Strategy({
  54. jwtFromRequest: securityHelper.extractJWT,
  55. secretOrKey: WIKI.config.certs.public,
  56. audience: WIKI.config.auth.audience,
  57. issuer: 'urn:wiki.js'
  58. }, (jwtPayload, cb) => {
  59. cb(null, jwtPayload)
  60. }))
  61. // Load enabled strategies
  62. const enabledStrategies = await WIKI.models.authentication.getStrategies()
  63. for (let idx in enabledStrategies) {
  64. const stg = enabledStrategies[idx]
  65. if (!stg.isEnabled) { continue }
  66. const strategy = require(`../modules/authentication/${stg.key}/authentication.js`)
  67. stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback`
  68. strategy.init(passport, stg.config)
  69. strategy.config = stg.config
  70. WIKI.auth.strategies[stg.key] = {
  71. ...strategy,
  72. ...stg
  73. }
  74. WIKI.logger.info(`Authentication Strategy ${stg.key}: [ OK ]`)
  75. }
  76. } catch (err) {
  77. WIKI.logger.error(`Authentication Strategy: [ FAILED ]`)
  78. WIKI.logger.error(err)
  79. }
  80. },
  81. /**
  82. * Authenticate current request
  83. *
  84. * @param {Express Request} req
  85. * @param {Express Response} res
  86. * @param {Express Next Callback} next
  87. */
  88. authenticate(req, res, next) {
  89. WIKI.auth.passport.authenticate('jwt', {session: false}, async (err, user, info) => {
  90. if (err) { return next() }
  91. // Expired but still valid within N days, just renew
  92. if (info instanceof Error && info.name === 'TokenExpiredError' && moment().subtract(14, 'days').isBefore(info.expiredAt)) {
  93. const jwtPayload = jwt.decode(securityHelper.extractJWT(req))
  94. try {
  95. const newToken = await WIKI.models.users.refreshToken(jwtPayload.id)
  96. user = newToken.user
  97. user.permissions = user.getGlobalPermissions()
  98. req.user = user
  99. // Try headers, otherwise cookies for response
  100. if (req.get('content-type') === 'application/json') {
  101. res.set('new-jwt', newToken.token)
  102. } else {
  103. res.cookie('jwt', newToken.token, { expires: moment().add(365, 'days').toDate() })
  104. }
  105. } catch (errc) {
  106. WIKI.logger.warn(errc)
  107. return next()
  108. }
  109. }
  110. // JWT is NOT valid, set as guest
  111. if (!user) {
  112. if (WIKI.auth.guest.cacheExpiration.isSameOrBefore(moment.utc())) {
  113. WIKI.auth.guest = await WIKI.models.users.getGuestUser()
  114. WIKI.auth.guest.cacheExpiration = moment.utc().add(1, 'm')
  115. }
  116. req.user = WIKI.auth.guest
  117. return next()
  118. }
  119. // JWT is valid
  120. req.logIn(user, { session: false }, (errc) => {
  121. if (errc) { return next(errc) }
  122. next()
  123. })
  124. })(req, res, next)
  125. },
  126. /**
  127. * Check if user has access to resource
  128. *
  129. * @param {User} user
  130. * @param {Array<String>} permissions
  131. * @param {String|Boolean} path
  132. */
  133. checkAccess(user, permissions = [], page = false) {
  134. const userPermissions = user.permissions ? user.permissions : user.getGlobalPermissions()
  135. // System Admin
  136. if (_.includes(userPermissions, 'manage:system')) {
  137. return true
  138. }
  139. // Check Global Permissions
  140. if (_.intersection(userPermissions, permissions).length < 1) {
  141. return false
  142. }
  143. // Check Page Rules
  144. if (page && user.groups) {
  145. let checkState = {
  146. deny: false,
  147. match: false,
  148. specificity: ''
  149. }
  150. user.groups.forEach(grp => {
  151. const grpId = _.isObject(grp) ? _.get(grp, 'id', 0) : grp
  152. _.get(WIKI.auth.groups, `${grpId}.pageRules`, []).forEach(rule => {
  153. switch (rule.match) {
  154. case 'START':
  155. if (_.startsWith(`/${page.path}`, `/${rule.path}`)) {
  156. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['END', 'REGEX', 'EXACT'] })
  157. }
  158. break
  159. case 'END':
  160. if (_.endsWith(page.path, rule.path)) {
  161. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['REGEX', 'EXACT'] })
  162. }
  163. break
  164. case 'REGEX':
  165. const reg = new RegExp(rule.path)
  166. if (reg.test(page.path)) {
  167. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['EXACT'] })
  168. }
  169. break
  170. case 'EXACT':
  171. if (`/${page.path}` === `/${rule.path}`) {
  172. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: [] })
  173. }
  174. break
  175. }
  176. })
  177. })
  178. return (checkState.match && !checkState.deny)
  179. }
  180. return false
  181. },
  182. /**
  183. * Check and apply Page Rule specificity
  184. *
  185. * @access private
  186. */
  187. _applyPageRuleSpecificity ({ rule, checkState, higherPriority = [] }) {
  188. if (rule.path.length === checkState.specificity.length) {
  189. // Do not override higher priority rules
  190. if (_.includes(higherPriority, checkState.match)) {
  191. return checkState
  192. }
  193. // Do not override a previous DENY rule with same match
  194. if (rule.match === checkState.match && checkState.deny && !rule.deny) {
  195. return checkState
  196. }
  197. } else if (rule.path.length < checkState.specificity.length) {
  198. // Do not override higher specificity rules
  199. return checkState
  200. }
  201. return {
  202. deny: rule.deny,
  203. match: rule.match,
  204. specificity: rule.path
  205. }
  206. },
  207. /**
  208. * Reload Groups from DB
  209. */
  210. async reloadGroups() {
  211. const groupsArray = await WIKI.models.groups.query()
  212. this.groups = _.keyBy(groupsArray, 'id')
  213. },
  214. /**
  215. * Generate New Authentication Public / Private Key Certificates
  216. */
  217. async regenerateCertificates() {
  218. WIKI.logger.info('Regenerating certificates...')
  219. _.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
  220. const certs = crypto.generateKeyPairSync('rsa', {
  221. modulusLength: 2048,
  222. publicKeyEncoding: {
  223. type: 'pkcs1',
  224. format: 'pem'
  225. },
  226. privateKeyEncoding: {
  227. type: 'pkcs1',
  228. format: 'pem',
  229. cipher: 'aes-256-cbc',
  230. passphrase: WIKI.config.sessionSecret
  231. }
  232. })
  233. _.set(WIKI.config, 'certs', {
  234. jwk: pem2jwk(certs.publicKey),
  235. public: certs.publicKey,
  236. private: certs.privateKey
  237. })
  238. await WIKI.configSvc.saveToDb([
  239. 'certs',
  240. 'sessionSecret'
  241. ])
  242. await WIKI.auth.activateStrategies()
  243. WIKI.logger.info('Regenerated certificates: [ COMPLETED ]')
  244. },
  245. /**
  246. * Reset Guest User
  247. */
  248. async resetGuestUser() {
  249. WIKI.logger.info('Resetting guest account...')
  250. const guestGroup = await WIKI.models.groups.query().where('id', 2).first()
  251. await WIKI.models.users.query().delete().where({
  252. providerKey: 'local',
  253. email: 'guest@example.com'
  254. }).orWhere('id', 2)
  255. const guestUser = await WIKI.models.users.query().insert({
  256. id: 2,
  257. provider: 'local',
  258. email: 'guest@example.com',
  259. name: 'Guest',
  260. password: '',
  261. locale: 'en',
  262. defaultEditor: 'markdown',
  263. tfaIsActive: false,
  264. isSystem: true,
  265. isActive: true,
  266. isVerified: true
  267. })
  268. await guestUser.$relatedQuery('groups').relate(guestGroup.id)
  269. WIKI.logger.info('Guest user has been reset: [ COMPLETED ]')
  270. }
  271. }