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.

240 lines
7.0 KiB

5 years ago
  1. const passport = require('passport')
  2. const passportJWT = require('passport-jwt')
  3. const _ = require('lodash')
  4. const path = require('path')
  5. const jwt = require('jsonwebtoken')
  6. const moment = require('moment')
  7. const securityHelper = require('../helpers/security')
  8. /* global WIKI */
  9. module.exports = {
  10. strategies: {},
  11. guest: {
  12. cacheExpiration: moment.utc().subtract(1, 'd')
  13. },
  14. groups: {},
  15. /**
  16. * Initialize the authentication module
  17. */
  18. init() {
  19. this.passport = passport
  20. passport.serializeUser((user, done) => {
  21. done(null, user.id)
  22. })
  23. passport.deserializeUser(async (id, done) => {
  24. try {
  25. const user = await WIKI.models.users.query().findById(id).modifyEager('groups', builder => {
  26. builder.select('groups.id', 'permissions')
  27. })
  28. if (user) {
  29. done(null, user)
  30. } else {
  31. done(new Error(WIKI.lang.t('auth:errors:usernotfound')), null)
  32. }
  33. } catch (err) {
  34. done(err, null)
  35. }
  36. })
  37. this.reloadGroups()
  38. return this
  39. },
  40. /**
  41. * Load authentication strategies
  42. */
  43. async activateStrategies() {
  44. try {
  45. // Unload any active strategies
  46. WIKI.auth.strategies = {}
  47. const currentStrategies = _.keys(passport._strategies)
  48. _.pull(currentStrategies, 'session')
  49. _.forEach(currentStrategies, stg => { passport.unuse(stg) })
  50. // Load JWT
  51. passport.use('jwt', new passportJWT.Strategy({
  52. jwtFromRequest: securityHelper.extractJWT,
  53. secretOrKey: WIKI.config.certs.public,
  54. audience: WIKI.config.auth.audience,
  55. issuer: 'urn:wiki.js'
  56. }, (jwtPayload, cb) => {
  57. cb(null, jwtPayload)
  58. }))
  59. // Load enabled strategies
  60. const enabledStrategies = await WIKI.models.authentication.getStrategies()
  61. for (let idx in enabledStrategies) {
  62. const stg = enabledStrategies[idx]
  63. if (!stg.isEnabled) { continue }
  64. const strategy = require(`../modules/authentication/${stg.key}/authentication.js`)
  65. stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback`
  66. strategy.init(passport, stg.config)
  67. strategy.config = stg.config
  68. WIKI.auth.strategies[stg.key] = {
  69. ...strategy,
  70. ...stg
  71. }
  72. WIKI.logger.info(`Authentication Strategy ${stg.key}: [ OK ]`)
  73. }
  74. } catch (err) {
  75. WIKI.logger.error(`Authentication Strategy: [ FAILED ]`)
  76. WIKI.logger.error(err)
  77. }
  78. },
  79. /**
  80. * Authenticate current request
  81. *
  82. * @param {Express Request} req
  83. * @param {Express Response} res
  84. * @param {Express Next Callback} next
  85. */
  86. authenticate(req, res, next) {
  87. WIKI.auth.passport.authenticate('jwt', {session: false}, async (err, user, info) => {
  88. if (err) { return next() }
  89. // Expired but still valid within N days, just renew
  90. if (info instanceof Error && info.name === 'TokenExpiredError' && moment().subtract(14, 'days').isBefore(info.expiredAt)) {
  91. const jwtPayload = jwt.decode(securityHelper.extractJWT(req))
  92. try {
  93. const newToken = await WIKI.models.users.refreshToken(jwtPayload.id)
  94. user = newToken.user
  95. user.permissions = user.getGlobalPermissions()
  96. req.user = user
  97. // Try headers, otherwise cookies for response
  98. if (req.get('content-type') === 'application/json') {
  99. res.set('new-jwt', newToken.token)
  100. } else {
  101. res.cookie('jwt', newToken.token, { expires: moment().add(365, 'days').toDate() })
  102. }
  103. } catch (err) {
  104. WIKI.logger.warn(err)
  105. return next()
  106. }
  107. }
  108. // JWT is NOT valid, set as guest
  109. if (!user) {
  110. if (WIKI.auth.guest.cacheExpiration.isSameOrBefore(moment.utc())) {
  111. WIKI.auth.guest = await WIKI.models.users.getGuestUser()
  112. WIKI.auth.guest.cacheExpiration = moment.utc().add(1, 'm')
  113. }
  114. req.user = WIKI.auth.guest
  115. return next()
  116. }
  117. // JWT is valid
  118. req.logIn(user, { session: false }, (err) => {
  119. if (err) { return next(err) }
  120. next()
  121. })
  122. })(req, res, next)
  123. },
  124. /**
  125. * Check if user has access to resource
  126. *
  127. * @param {User} user
  128. * @param {Array<String>} permissions
  129. * @param {String|Boolean} path
  130. */
  131. checkAccess(user, permissions = [], page = false) {
  132. const userPermissions = user.permissions ? user.permissions : user.getGlobalPermissions()
  133. // System Admin
  134. if (_.includes(userPermissions, 'manage:system')) {
  135. return true
  136. }
  137. // Check Global Permissions
  138. if (_.intersection(userPermissions, permissions).length < 1) {
  139. return false
  140. }
  141. // Check Page Rules
  142. if (path && user.groups) {
  143. let checkState = {
  144. deny: false,
  145. match: false,
  146. specificity: ''
  147. }
  148. user.groups.forEach(grp => {
  149. const grpId = _.isObject(grp) ? _.get(grp, 'id', 0) : grp
  150. _.get(WIKI.auth.groups, `${grpId}.pageRules`, []).forEach(rule => {
  151. switch (rule.match) {
  152. case 'START':
  153. if (_.startsWith(`/${page.path}`, `/${rule.path}`)) {
  154. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['END', 'REGEX', 'EXACT'] })
  155. }
  156. break
  157. case 'END':
  158. if (_.endsWith(page.path, rule.path)) {
  159. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['REGEX', 'EXACT'] })
  160. }
  161. break
  162. case 'REGEX':
  163. const reg = new RegExp(rule.path)
  164. if (reg.test(page.path)) {
  165. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['EXACT'] })
  166. }
  167. break
  168. case 'EXACT':
  169. if (`/${page.path}` === `/${rule.path}`) {
  170. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: [] })
  171. }
  172. break
  173. }
  174. })
  175. })
  176. return (checkState.match && !checkState.deny)
  177. }
  178. return false
  179. },
  180. /**
  181. * Check and apply Page Rule specificity
  182. *
  183. * @access private
  184. */
  185. _applyPageRuleSpecificity ({ rule, checkState, higherPriority = [] }) {
  186. if (rule.path.length === checkState.specificity.length) {
  187. // Do not override higher priority rules
  188. if (_.includes(higherPriority, checkState.match)) {
  189. return checkState
  190. }
  191. // Do not override a previous DENY rule with same match
  192. if (rule.match === checkState.match && checkState.deny && !rule.deny) {
  193. return checkState
  194. }
  195. } else if (rule.path.length < checkState.specificity.length) {
  196. // Do not override higher specificity rules
  197. return checkState
  198. }
  199. return {
  200. deny: rule.deny,
  201. match: rule.match,
  202. specificity: rule.path
  203. }
  204. },
  205. /**
  206. * Reload Groups from DB
  207. */
  208. async reloadGroups() {
  209. const groupsArray = await WIKI.models.groups.query()
  210. this.groups = _.keyBy(groupsArray, 'id')
  211. }
  212. }