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.

322 lines
9.3 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).withGraphFetched('groups').modifyGraph('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. if (_.intersection(rule.roles, permissions).length > 0) {
  154. switch (rule.match) {
  155. case 'START':
  156. if (_.startsWith(`/${page.path}`, `/${rule.path}`)) {
  157. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['END', 'REGEX', 'EXACT', 'TAG'] })
  158. }
  159. break
  160. case 'END':
  161. if (_.endsWith(page.path, rule.path)) {
  162. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['REGEX', 'EXACT', 'TAG'] })
  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', 'TAG'] })
  169. }
  170. break
  171. case 'TAG':
  172. _.get(page, 'tags', []).forEach(tag => {
  173. if (tag.tag === rule.path) {
  174. checkState = this._applyPageRuleSpecificity({
  175. rule,
  176. checkState,
  177. higherPriority: ['EXACT']
  178. })
  179. }
  180. })
  181. break
  182. case 'EXACT':
  183. if (`/${page.path}` === `/${rule.path}`) {
  184. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: [] })
  185. }
  186. break
  187. }
  188. }
  189. })
  190. })
  191. return (checkState.match && !checkState.deny)
  192. }
  193. return false
  194. },
  195. /**
  196. * Check and apply Page Rule specificity
  197. *
  198. * @access private
  199. */
  200. _applyPageRuleSpecificity ({ rule, checkState, higherPriority = [] }) {
  201. if (rule.path.length === checkState.specificity.length) {
  202. // Do not override higher priority rules
  203. if (_.includes(higherPriority, checkState.match)) {
  204. return checkState
  205. }
  206. // Do not override a previous DENY rule with same match
  207. if (rule.match === checkState.match && checkState.deny && !rule.deny) {
  208. return checkState
  209. }
  210. } else if (rule.path.length < checkState.specificity.length) {
  211. // Do not override higher specificity rules
  212. return checkState
  213. }
  214. return {
  215. deny: rule.deny,
  216. match: rule.match,
  217. specificity: rule.path
  218. }
  219. },
  220. /**
  221. * Reload Groups from DB
  222. */
  223. async reloadGroups() {
  224. const groupsArray = await WIKI.models.groups.query()
  225. this.groups = _.keyBy(groupsArray, 'id')
  226. },
  227. /**
  228. * Generate New Authentication Public / Private Key Certificates
  229. */
  230. async regenerateCertificates() {
  231. WIKI.logger.info('Regenerating certificates...')
  232. _.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
  233. const certs = crypto.generateKeyPairSync('rsa', {
  234. modulusLength: 2048,
  235. publicKeyEncoding: {
  236. type: 'pkcs1',
  237. format: 'pem'
  238. },
  239. privateKeyEncoding: {
  240. type: 'pkcs1',
  241. format: 'pem',
  242. cipher: 'aes-256-cbc',
  243. passphrase: WIKI.config.sessionSecret
  244. }
  245. })
  246. _.set(WIKI.config, 'certs', {
  247. jwk: pem2jwk(certs.publicKey),
  248. public: certs.publicKey,
  249. private: certs.privateKey
  250. })
  251. await WIKI.configSvc.saveToDb([
  252. 'certs',
  253. 'sessionSecret'
  254. ])
  255. await WIKI.auth.activateStrategies()
  256. WIKI.logger.info('Regenerated certificates: [ COMPLETED ]')
  257. },
  258. /**
  259. * Reset Guest User
  260. */
  261. async resetGuestUser() {
  262. WIKI.logger.info('Resetting guest account...')
  263. const guestGroup = await WIKI.models.groups.query().where('id', 2).first()
  264. await WIKI.models.users.query().delete().where({
  265. providerKey: 'local',
  266. email: 'guest@example.com'
  267. }).orWhere('id', 2)
  268. const guestUser = await WIKI.models.users.query().insert({
  269. id: 2,
  270. provider: 'local',
  271. email: 'guest@example.com',
  272. name: 'Guest',
  273. password: '',
  274. locale: 'en',
  275. defaultEditor: 'markdown',
  276. tfaIsActive: false,
  277. isSystem: true,
  278. isActive: true,
  279. isVerified: true
  280. })
  281. await guestUser.$relatedQuery('groups').relate(guestGroup.id)
  282. WIKI.logger.info('Guest user has been reset: [ COMPLETED ]')
  283. }
  284. }