2
0

auth.js 11 KB

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