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.

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