auth.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  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 { DateTime } = require('luxon')
  7. const util = require('node:util')
  8. const crypto = require('node:crypto')
  9. const randomBytes = util.promisify(crypto.randomBytes)
  10. const pem2jwk = require('pem-jwk').pem2jwk
  11. const securityHelper = require('../helpers/security')
  12. module.exports = {
  13. strategies: {},
  14. guest: {
  15. cacheExpiration: DateTime.utc().minus({ days: 1 })
  16. },
  17. groups: {},
  18. validApiKeys: [],
  19. revocationList: 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.db.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.auth.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.db.authentication.getStrategies({ enabledOnly: true })
  68. for (const stg of enabledStrategies) {
  69. try {
  70. const strategy = require(`../modules/authentication/${stg.module}/authentication.js`)
  71. stg.config.callbackURL = `${WIKI.config.host}/login/${stg.id}/callback`
  72. stg.config.key = stg.id
  73. strategy.init(passport, stg.config)
  74. strategy.config = stg.config
  75. WIKI.auth.strategies[stg.key] = {
  76. ...strategy,
  77. ...stg
  78. }
  79. WIKI.logger.info(`Authentication Strategy ${stg.displayName}: [ OK ]`)
  80. } catch (err) {
  81. WIKI.logger.error(`Authentication Strategy ${stg.displayName} (${stg.id}): [ FAILED ]`)
  82. WIKI.logger.error(err)
  83. }
  84. }
  85. } catch (err) {
  86. WIKI.logger.error(`Failed to initialize Authentication Strategies: [ ERROR ]`)
  87. WIKI.logger.error(err)
  88. }
  89. },
  90. /**
  91. * Authenticate current request
  92. *
  93. * @param {Express Request} req
  94. * @param {Express Response} res
  95. * @param {Express Next Callback} next
  96. */
  97. authenticate (req, res, next) {
  98. WIKI.auth.passport.authenticate('jwt', {session: false}, async (err, user, info) => {
  99. if (err) { return next() }
  100. let mustRevalidate = false
  101. const strategyId = user.pvd
  102. // Expired but still valid within N days, just renew
  103. if (info instanceof Error && info.name === 'TokenExpiredError') {
  104. const expiredDate = (info.expiredAt instanceof Date) ? info.expiredAt.toISOString() : info.expiredAt
  105. if (DateTime.utc().minus(ms(WIKI.config.auth.tokenRenewal)) < DateTime.fromISO(expiredDate)) {
  106. mustRevalidate = true
  107. }
  108. }
  109. // Check if user / group is in revocation list
  110. if (user && !user.api && !mustRevalidate) {
  111. const uRevalidate = WIKI.auth.revocationList.get(`u${_.toString(user.id)}`)
  112. if (uRevalidate && user.iat < uRevalidate) {
  113. mustRevalidate = true
  114. } else if (DateTime.fromSeconds(user.iat) <= WIKI.startedAt) { // Prevent new / restarted instance from allowing revoked tokens
  115. mustRevalidate = true
  116. } else {
  117. for (const gid of user.groups) {
  118. const gRevalidate = WIKI.auth.revocationList.get(`g${_.toString(gid)}`)
  119. if (gRevalidate && user.iat < gRevalidate) {
  120. mustRevalidate = true
  121. break
  122. }
  123. }
  124. }
  125. }
  126. // Revalidate and renew token
  127. if (mustRevalidate) {
  128. const jwtPayload = jwt.decode(securityHelper.extractJWT(req))
  129. try {
  130. const newToken = await WIKI.db.users.refreshToken(jwtPayload.id, jwtPayload.pvd)
  131. user = newToken.user
  132. user.permissions = user.getPermissions()
  133. user.groups = user.getGroups()
  134. user.strategyId = strategyId
  135. req.user = user
  136. // Try headers, otherwise cookies for response
  137. if (req.get('content-type') === 'application/json') {
  138. res.set('new-jwt', newToken.token)
  139. } else {
  140. res.cookie('jwt', newToken.token, { expires: DateTime.utc().plus({ days: 365 }).toJSDate() })
  141. }
  142. } catch (errc) {
  143. WIKI.logger.warn(errc)
  144. return next()
  145. }
  146. } else if (user) {
  147. user = await WIKI.db.users.getById(user.id)
  148. user.permissions = user.getPermissions()
  149. user.groups = user.getGroups()
  150. user.strategyId = strategyId
  151. req.user = user
  152. } else {
  153. // JWT is NOT valid, set as guest
  154. if (WIKI.auth.guest.cacheExpiration <= DateTime.utc()) {
  155. WIKI.auth.guest = await WIKI.db.users.getGuestUser()
  156. WIKI.auth.guest.cacheExpiration = DateTime.utc().plus({ minutes: 1 })
  157. }
  158. req.user = WIKI.auth.guest
  159. return next()
  160. }
  161. // Process API tokens
  162. if (_.has(user, 'api')) {
  163. if (!WIKI.config.api.isEnabled) {
  164. return next(new Error('API is disabled. You must enable it from the Administration Area first.'))
  165. } else if (_.includes(WIKI.auth.validApiKeys, user.api)) {
  166. req.user = {
  167. id: 1,
  168. email: 'api@localhost',
  169. name: 'API',
  170. pictureUrl: null,
  171. timezone: 'America/New_York',
  172. localeCode: 'en',
  173. permissions: _.get(WIKI.auth.groups, `${user.grp}.permissions`, []),
  174. groups: [user.grp],
  175. getPermissions () {
  176. return req.user.permissions
  177. },
  178. getGroups () {
  179. return req.user.groups
  180. }
  181. }
  182. return next()
  183. } else {
  184. return next(new Error('API Key is invalid or was revoked.'))
  185. }
  186. }
  187. // JWT is valid
  188. req.logIn(user, { session: false }, (errc) => {
  189. if (errc) { return next(errc) }
  190. next()
  191. })
  192. })(req, res, next)
  193. },
  194. /**
  195. * Check if user has access to resource
  196. *
  197. * @param {User} user
  198. * @param {Array<String>} permissions
  199. * @param {String|Boolean} path
  200. */
  201. checkAccess(user, permissions = [], page = false) {
  202. const userPermissions = user.permissions ? user.permissions : user.getPermissions()
  203. // System Admin
  204. if (_.includes(userPermissions, 'manage:system')) {
  205. return true
  206. }
  207. // Check Global Permissions
  208. if (_.intersection(userPermissions, permissions).length < 1) {
  209. return false
  210. }
  211. // Skip if no page rule to check
  212. if (!page) {
  213. return true
  214. }
  215. // Check Page Rules
  216. if (user.groups) {
  217. let checkState = {
  218. deny: false,
  219. match: false,
  220. specificity: ''
  221. }
  222. user.groups.forEach(grp => {
  223. const grpId = _.isObject(grp) ? _.get(grp, 'id', 0) : grp
  224. _.get(WIKI.auth.groups, `${grpId}.pageRules`, []).forEach(rule => {
  225. if (rule.locales && rule.locales.length > 0) {
  226. if (!rule.locales.includes(page.locale)) { return }
  227. }
  228. if (_.intersection(rule.roles, permissions).length > 0) {
  229. switch (rule.match) {
  230. case 'START':
  231. if (_.startsWith(`/${page.path}`, `/${rule.path}`)) {
  232. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['END', 'REGEX', 'EXACT', 'TAG'] })
  233. }
  234. break
  235. case 'END':
  236. if (_.endsWith(page.path, rule.path)) {
  237. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['REGEX', 'EXACT', 'TAG'] })
  238. }
  239. break
  240. case 'REGEX':
  241. const reg = new RegExp(rule.path)
  242. if (reg.test(page.path)) {
  243. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['EXACT', 'TAG'] })
  244. }
  245. break
  246. case 'TAG':
  247. _.get(page, 'tags', []).forEach(tag => {
  248. if (tag.tag === rule.path) {
  249. checkState = this._applyPageRuleSpecificity({
  250. rule,
  251. checkState,
  252. higherPriority: ['EXACT']
  253. })
  254. }
  255. })
  256. break
  257. case 'EXACT':
  258. if (`/${page.path}` === `/${rule.path}`) {
  259. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: [] })
  260. }
  261. break
  262. }
  263. }
  264. })
  265. })
  266. return (checkState.match && !checkState.deny)
  267. }
  268. return false
  269. },
  270. /**
  271. * Check for exclusive permissions (contain any X permission(s) but not any Y permission(s))
  272. *
  273. * @param {User} user
  274. * @param {Array<String>} includePermissions
  275. * @param {Array<String>} excludePermissions
  276. */
  277. checkExclusiveAccess(user, includePermissions = [], excludePermissions = []) {
  278. const userPermissions = user.permissions ? user.permissions : user.getPermissions()
  279. // Check Inclusion Permissions
  280. if (_.intersection(userPermissions, includePermissions).length < 1) {
  281. return false
  282. }
  283. // Check Exclusion Permissions
  284. if (_.intersection(userPermissions, excludePermissions).length > 0) {
  285. return false
  286. }
  287. return true
  288. },
  289. /**
  290. * Check and apply Page Rule specificity
  291. *
  292. * @access private
  293. */
  294. _applyPageRuleSpecificity ({ rule, checkState, higherPriority = [] }) {
  295. if (rule.path.length === checkState.specificity.length) {
  296. // Do not override higher priority rules
  297. if (_.includes(higherPriority, checkState.match)) {
  298. return checkState
  299. }
  300. // Do not override a previous DENY rule with same match
  301. if (rule.match === checkState.match && checkState.deny && !rule.deny) {
  302. return checkState
  303. }
  304. } else if (rule.path.length < checkState.specificity.length) {
  305. // Do not override higher specificity rules
  306. return checkState
  307. }
  308. return {
  309. deny: rule.deny,
  310. match: rule.match,
  311. specificity: rule.path
  312. }
  313. },
  314. /**
  315. * Reload Groups from DB
  316. */
  317. async reloadGroups () {
  318. const groupsArray = await WIKI.db.groups.query()
  319. this.groups = _.keyBy(groupsArray, 'id')
  320. WIKI.auth.guest.cacheExpiration = DateTime.utc().minus({ days: 1 })
  321. },
  322. /**
  323. * Reload valid API Keys from DB
  324. */
  325. async reloadApiKeys () {
  326. const keys = await WIKI.db.apiKeys.query().select('id').where('isRevoked', false).andWhere('expiration', '>', DateTime.utc().toISO())
  327. this.validApiKeys = _.map(keys, 'id')
  328. },
  329. /**
  330. * Generate New Authentication Public / Private Key Certificates
  331. */
  332. async regenerateCertificates () {
  333. WIKI.logger.info('Regenerating certificates...')
  334. _.set(WIKI.config, 'sessionSecret', (await randomBytes(32)).toString('hex'))
  335. const certs = crypto.generateKeyPairSync('rsa', {
  336. modulusLength: 2048,
  337. publicKeyEncoding: {
  338. type: 'pkcs1',
  339. format: 'pem'
  340. },
  341. privateKeyEncoding: {
  342. type: 'pkcs1',
  343. format: 'pem',
  344. cipher: 'aes-256-cbc',
  345. passphrase: WIKI.config.sessionSecret
  346. }
  347. })
  348. _.set(WIKI.config, 'certs', {
  349. jwk: pem2jwk(certs.publicKey),
  350. public: certs.publicKey,
  351. private: certs.privateKey
  352. })
  353. await WIKI.configSvc.saveToDb([
  354. 'certs',
  355. 'sessionSecret'
  356. ])
  357. await WIKI.auth.activateStrategies()
  358. WIKI.events.outbound.emit('reloadAuthStrategies')
  359. WIKI.logger.info('Regenerated certificates: [ COMPLETED ]')
  360. },
  361. /**
  362. * Reset Guest User
  363. */
  364. async resetGuestUser() {
  365. WIKI.logger.info('Resetting guest account...')
  366. const guestGroup = await WIKI.db.groups.query().where('id', 2).first()
  367. await WIKI.db.users.query().delete().where({
  368. providerKey: 'local',
  369. email: 'guest@example.com'
  370. }).orWhere('id', 2)
  371. const guestUser = await WIKI.db.users.query().insert({
  372. id: 2,
  373. provider: 'local',
  374. email: 'guest@example.com',
  375. name: 'Guest',
  376. password: '',
  377. locale: 'en',
  378. defaultEditor: 'markdown',
  379. tfaIsActive: false,
  380. isSystem: true,
  381. isActive: true,
  382. isVerified: true
  383. })
  384. await guestUser.$relatedQuery('groups').relate(guestGroup.id)
  385. WIKI.logger.info('Guest user has been reset: [ COMPLETED ]')
  386. },
  387. /**
  388. * Subscribe to HA propagation events
  389. */
  390. subscribeToEvents() {
  391. WIKI.events.inbound.on('reloadGroups', () => {
  392. WIKI.auth.reloadGroups()
  393. })
  394. WIKI.events.inbound.on('reloadApiKeys', () => {
  395. WIKI.auth.reloadApiKeys()
  396. })
  397. WIKI.events.inbound.on('reloadAuthStrategies', () => {
  398. WIKI.auth.activateStrategies()
  399. })
  400. WIKI.events.inbound.on('addAuthRevoke', (args) => {
  401. WIKI.auth.revokeUserTokens(args)
  402. })
  403. },
  404. /**
  405. * Get all user permissions for a specific page
  406. */
  407. getEffectivePermissions (req, page) {
  408. return {
  409. comments: {
  410. read: WIKI.auth.checkAccess(req.user, ['read:comments'], page),
  411. write: WIKI.auth.checkAccess(req.user, ['write:comments'], page),
  412. manage: WIKI.auth.checkAccess(req.user, ['manage:comments'], page)
  413. },
  414. history: {
  415. read: WIKI.auth.checkAccess(req.user, ['read:history'], page)
  416. },
  417. source: {
  418. read: WIKI.auth.checkAccess(req.user, ['read:source'], page)
  419. },
  420. pages: {
  421. read: WIKI.auth.checkAccess(req.user, ['read:pages'], page),
  422. write: WIKI.auth.checkAccess(req.user, ['write:pages'], page),
  423. manage: WIKI.auth.checkAccess(req.user, ['manage:pages'], page),
  424. delete: WIKI.auth.checkAccess(req.user, ['delete:pages'], page),
  425. script: WIKI.auth.checkAccess(req.user, ['write:scripts'], page),
  426. style: WIKI.auth.checkAccess(req.user, ['write:styles'], page)
  427. },
  428. system: {
  429. manage: WIKI.auth.checkAccess(req.user, ['manage:system'], page)
  430. }
  431. }
  432. },
  433. /**
  434. * Add user / group ID to JWT revocation list, forcing all requests to be validated against the latest permissions
  435. */
  436. revokeUserTokens ({ id, kind = 'u' }) {
  437. WIKI.auth.revocationList.set(`${kind}${_.toString(id)}`, Math.round(DateTime.utc().minus({ seconds: 5 }).toSeconds()), Math.ceil(ms(WIKI.config.auth.tokenExpiration) / 1000))
  438. }
  439. }