auth.js 15 KB

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