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.

377 lines
11 KiB

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