auth.mjs 15 KB

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