auth.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. const passport = require('passport')
  2. const passportJWT = require('passport-jwt')
  3. const _ = require('lodash')
  4. const jwt = require('jsonwebtoken')
  5. const ms = require('ms')
  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. validApiKeys: [],
  19. revokationList: require('./cache').init(),
  20. /**
  21. * Initialize the authentication module
  22. */
  23. init() {
  24. this.passport = passport
  25. passport.serializeUser((user, done) => {
  26. done(null, user.id)
  27. })
  28. passport.deserializeUser(async (id, done) => {
  29. try {
  30. const user = await WIKI.models.users.query().findById(id).withGraphFetched('groups').modifyGraph('groups', builder => {
  31. builder.select('groups.id', 'permissions')
  32. })
  33. if (user) {
  34. done(null, user)
  35. } else {
  36. done(new Error(WIKI.lang.t('auth:errors:usernotfound')), null)
  37. }
  38. } catch (err) {
  39. done(err, null)
  40. }
  41. })
  42. this.reloadGroups()
  43. this.reloadApiKeys()
  44. return this
  45. },
  46. /**
  47. * Load authentication strategies
  48. */
  49. async activateStrategies() {
  50. try {
  51. // Unload any active strategies
  52. WIKI.auth.strategies = {}
  53. const currentStrategies = _.keys(passport._strategies)
  54. _.pull(currentStrategies, 'session')
  55. _.forEach(currentStrategies, stg => { passport.unuse(stg) })
  56. // Load JWT
  57. passport.use('jwt', new passportJWT.Strategy({
  58. jwtFromRequest: securityHelper.extractJWT,
  59. secretOrKey: WIKI.config.certs.public,
  60. audience: WIKI.config.auth.audience,
  61. issuer: 'urn:wiki.js',
  62. algorithms: ['RS256']
  63. }, (jwtPayload, cb) => {
  64. cb(null, jwtPayload)
  65. }))
  66. // Load enabled strategies
  67. const enabledStrategies = await WIKI.models.authentication.getStrategies()
  68. for (let idx in enabledStrategies) {
  69. const stg = enabledStrategies[idx]
  70. if (!stg.isEnabled) { continue }
  71. try {
  72. const strategy = require(`../modules/authentication/${stg.key}/authentication.js`)
  73. stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback`
  74. strategy.init(passport, stg.config)
  75. strategy.config = stg.config
  76. WIKI.auth.strategies[stg.key] = {
  77. ...strategy,
  78. ...stg
  79. }
  80. WIKI.logger.info(`Authentication Strategy ${stg.key}: [ OK ]`)
  81. } catch (err) {
  82. WIKI.logger.error(`Authentication Strategy ${stg.key}: [ FAILED ]`)
  83. WIKI.logger.error(err)
  84. }
  85. }
  86. } catch (err) {
  87. WIKI.logger.error(`Failed to initialize Authentication Strategies: [ ERROR ]`)
  88. WIKI.logger.error(err)
  89. }
  90. },
  91. /**
  92. * Authenticate current request
  93. *
  94. * @param {Express Request} req
  95. * @param {Express Response} res
  96. * @param {Express Next Callback} next
  97. */
  98. authenticate(req, res, next) {
  99. WIKI.auth.passport.authenticate('jwt', {session: false}, async (err, user, info) => {
  100. if (err) { return next() }
  101. let mustRevalidate = false
  102. // Expired but still valid within N days, just renew
  103. if (info instanceof Error && info.name === 'TokenExpiredError' && moment().subtract(ms(WIKI.config.auth.tokenRenewal), 'ms').isBefore(info.expiredAt)) {
  104. mustRevalidate = true
  105. }
  106. // Check if user / group is in revokation list
  107. if (user) {
  108. if (WIKI.auth.revokationList.has(`u${_.toString(user.id)}`)) {
  109. mustRevalidate = true
  110. }
  111. for (const gid of user.groups) {
  112. if (WIKI.auth.revokationList.has(`g${_.toString(gid)}`)) {
  113. mustRevalidate = true
  114. }
  115. }
  116. }
  117. // Revalidate and renew token
  118. if (mustRevalidate) {
  119. console.info('MUST REVALIDATE')
  120. const jwtPayload = jwt.decode(securityHelper.extractJWT(req))
  121. try {
  122. const newToken = await WIKI.models.users.refreshToken(jwtPayload.id)
  123. user = newToken.user
  124. user.permissions = user.getGlobalPermissions()
  125. user.groups = user.getGroups()
  126. req.user = user
  127. // Try headers, otherwise cookies for response
  128. if (req.get('content-type') === 'application/json') {
  129. res.set('new-jwt', newToken.token)
  130. } else {
  131. res.cookie('jwt', newToken.token, { expires: moment().add(365, 'days').toDate() })
  132. }
  133. } catch (errc) {
  134. WIKI.logger.warn(errc)
  135. return next()
  136. }
  137. }
  138. // JWT is NOT valid, set as guest
  139. if (!user) {
  140. if (WIKI.auth.guest.cacheExpiration.isSameOrBefore(moment.utc())) {
  141. WIKI.auth.guest = await WIKI.models.users.getGuestUser()
  142. WIKI.auth.guest.cacheExpiration = moment.utc().add(1, 'm')
  143. }
  144. req.user = WIKI.auth.guest
  145. return next()
  146. }
  147. // Process API tokens
  148. if (_.has(user, 'api')) {
  149. if (!WIKI.config.api.isEnabled) {
  150. return next(new Error('API is disabled. You must enable it from the Administration Area first.'))
  151. } else if (_.includes(WIKI.auth.validApiKeys, user.api)) {
  152. req.user = {
  153. id: 1,
  154. email: 'api@localhost',
  155. name: 'API',
  156. pictureUrl: null,
  157. timezone: 'America/New_York',
  158. localeCode: 'en',
  159. permissions: _.get(WIKI.auth.groups, `${user.grp}.permissions`, []),
  160. groups: [user.grp],
  161. getGlobalPermissions () {
  162. return req.user.permissions
  163. },
  164. getGroups () {
  165. return req.user.groups
  166. }
  167. }
  168. return next()
  169. } else {
  170. return next(new Error('API Key is invalid or was revoked.'))
  171. }
  172. }
  173. // JWT is valid
  174. req.logIn(user, { session: false }, (errc) => {
  175. if (errc) { return next(errc) }
  176. next()
  177. })
  178. })(req, res, next)
  179. },
  180. /**
  181. * Check if user has access to resource
  182. *
  183. * @param {User} user
  184. * @param {Array<String>} permissions
  185. * @param {String|Boolean} path
  186. */
  187. checkAccess(user, permissions = [], page = false) {
  188. const userPermissions = user.permissions ? user.permissions : user.getGlobalPermissions()
  189. // System Admin
  190. if (_.includes(userPermissions, 'manage:system')) {
  191. return true
  192. }
  193. // Check Global Permissions
  194. if (_.intersection(userPermissions, permissions).length < 1) {
  195. return false
  196. }
  197. // Check Page Rules
  198. if (page && user.groups) {
  199. let checkState = {
  200. deny: false,
  201. match: false,
  202. specificity: ''
  203. }
  204. user.groups.forEach(grp => {
  205. const grpId = _.isObject(grp) ? _.get(grp, 'id', 0) : grp
  206. _.get(WIKI.auth.groups, `${grpId}.pageRules`, []).forEach(rule => {
  207. if (_.intersection(rule.roles, permissions).length > 0) {
  208. switch (rule.match) {
  209. case 'START':
  210. if (_.startsWith(`/${page.path}`, `/${rule.path}`)) {
  211. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['END', 'REGEX', 'EXACT', 'TAG'] })
  212. }
  213. break
  214. case 'END':
  215. if (_.endsWith(page.path, rule.path)) {
  216. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['REGEX', 'EXACT', 'TAG'] })
  217. }
  218. break
  219. case 'REGEX':
  220. const reg = new RegExp(rule.path)
  221. if (reg.test(page.path)) {
  222. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['EXACT', 'TAG'] })
  223. }
  224. break
  225. case 'TAG':
  226. _.get(page, 'tags', []).forEach(tag => {
  227. if (tag.tag === rule.path) {
  228. checkState = this._applyPageRuleSpecificity({
  229. rule,
  230. checkState,
  231. higherPriority: ['EXACT']
  232. })
  233. }
  234. })
  235. break
  236. case 'EXACT':
  237. if (`/${page.path}` === `/${rule.path}`) {
  238. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: [] })
  239. }
  240. break
  241. }
  242. }
  243. })
  244. })
  245. return (checkState.match && !checkState.deny)
  246. }
  247. return false
  248. },
  249. /**
  250. * Check and apply Page Rule specificity
  251. *
  252. * @access private
  253. */
  254. _applyPageRuleSpecificity ({ rule, checkState, higherPriority = [] }) {
  255. if (rule.path.length === checkState.specificity.length) {
  256. // Do not override higher priority rules
  257. if (_.includes(higherPriority, checkState.match)) {
  258. return checkState
  259. }
  260. // Do not override a previous DENY rule with same match
  261. if (rule.match === checkState.match && checkState.deny && !rule.deny) {
  262. return checkState
  263. }
  264. } else if (rule.path.length < checkState.specificity.length) {
  265. // Do not override higher specificity rules
  266. return checkState
  267. }
  268. return {
  269. deny: rule.deny,
  270. match: rule.match,
  271. specificity: rule.path
  272. }
  273. },
  274. /**
  275. * Reload Groups from DB
  276. */
  277. async reloadGroups () {
  278. const groupsArray = await WIKI.models.groups.query()
  279. this.groups = _.keyBy(groupsArray, 'id')
  280. WIKI.auth.guest.cacheExpiration = moment.utc().subtract(1, 'd')
  281. },
  282. /**
  283. * Reload valid API Keys from DB
  284. */
  285. async reloadApiKeys () {
  286. const keys = await WIKI.models.apiKeys.query().select('id').where('isRevoked', false).andWhere('expiration', '>', moment.utc().toISOString())
  287. this.validApiKeys = _.map(keys, 'id')
  288. },
  289. /**
  290. * Generate New Authentication Public / Private Key Certificates
  291. */
  292. async regenerateCertificates () {
  293. WIKI.logger.info('Regenerating certificates...')
  294. _.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
  295. const certs = crypto.generateKeyPairSync('rsa', {
  296. modulusLength: 2048,
  297. publicKeyEncoding: {
  298. type: 'pkcs1',
  299. format: 'pem'
  300. },
  301. privateKeyEncoding: {
  302. type: 'pkcs1',
  303. format: 'pem',
  304. cipher: 'aes-256-cbc',
  305. passphrase: WIKI.config.sessionSecret
  306. }
  307. })
  308. _.set(WIKI.config, 'certs', {
  309. jwk: pem2jwk(certs.publicKey),
  310. public: certs.publicKey,
  311. private: certs.privateKey
  312. })
  313. await WIKI.configSvc.saveToDb([
  314. 'certs',
  315. 'sessionSecret'
  316. ])
  317. await WIKI.auth.activateStrategies()
  318. WIKI.events.outbound.emit('reloadAuthStrategies')
  319. WIKI.logger.info('Regenerated certificates: [ COMPLETED ]')
  320. },
  321. /**
  322. * Reset Guest User
  323. */
  324. async resetGuestUser() {
  325. WIKI.logger.info('Resetting guest account...')
  326. const guestGroup = await WIKI.models.groups.query().where('id', 2).first()
  327. await WIKI.models.users.query().delete().where({
  328. providerKey: 'local',
  329. email: 'guest@example.com'
  330. }).orWhere('id', 2)
  331. const guestUser = await WIKI.models.users.query().insert({
  332. id: 2,
  333. provider: 'local',
  334. email: 'guest@example.com',
  335. name: 'Guest',
  336. password: '',
  337. locale: 'en',
  338. defaultEditor: 'markdown',
  339. tfaIsActive: false,
  340. isSystem: true,
  341. isActive: true,
  342. isVerified: true
  343. })
  344. await guestUser.$relatedQuery('groups').relate(guestGroup.id)
  345. WIKI.logger.info('Guest user has been reset: [ COMPLETED ]')
  346. },
  347. /**
  348. * Subscribe to HA propagation events
  349. */
  350. subscribeToEvents() {
  351. WIKI.events.inbound.on('reloadGroups', () => {
  352. WIKI.auth.reloadGroups()
  353. })
  354. WIKI.events.inbound.on('reloadApiKeys', () => {
  355. WIKI.auth.reloadApiKeys()
  356. })
  357. WIKI.events.inbound.on('reloadAuthStrategies', () => {
  358. WIKI.auth.activateStrategies()
  359. })
  360. WIKI.events.inbound.on('addAuthRevoke', (args) => {
  361. WIKI.auth.revokeUserTokens(args)
  362. })
  363. },
  364. /**
  365. * Get all user permissions for a specific page
  366. */
  367. getEffectivePermissions (req, page) {
  368. return {
  369. comments: {
  370. read: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['read:comments'], page) : false,
  371. write: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['write:comments'], page) : false,
  372. manage: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['manage:comments'], page) : false
  373. },
  374. history: {
  375. read: WIKI.auth.checkAccess(req.user, ['read:history'], page)
  376. },
  377. source: {
  378. read: WIKI.auth.checkAccess(req.user, ['read:source'], page)
  379. },
  380. pages: {
  381. read: WIKI.auth.checkAccess(req.user, ['read:pages'], page),
  382. write: WIKI.auth.checkAccess(req.user, ['write:pages'], page),
  383. manage: WIKI.auth.checkAccess(req.user, ['manage:pages'], page),
  384. delete: WIKI.auth.checkAccess(req.user, ['delete:pages'], page),
  385. script: WIKI.auth.checkAccess(req.user, ['write:scripts'], page),
  386. style: WIKI.auth.checkAccess(req.user, ['write:styles'], page)
  387. },
  388. system: {
  389. manage: WIKI.auth.checkAccess(req.user, ['manage:system'], page)
  390. }
  391. }
  392. },
  393. /**
  394. * Add user / group ID to JWT revokation list, forcing all requests to be validated against the latest permissions
  395. */
  396. revokeUserTokens ({ id, kind = 'u' }) {
  397. console.info(Math.ceil(ms(WIKI.config.auth.tokenRenewal) / 1000))
  398. WIKI.auth.revokationList.set(`${kind}${_.toString(id)}`, true, Math.ceil(ms(WIKI.config.auth.tokenRenewal) / 1000))
  399. }
  400. }