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.

485 lines
15 KiB

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