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.
wiki/server/core/auth.js

247 lines
7.2 KiB

const passport = require('passport')
const passportJWT = require('passport-jwt')
const fs = require('fs-extra')
const _ = require('lodash')
const path = require('path')
const jwt = require('jsonwebtoken')
const moment = require('moment')
const securityHelper = require('../helpers/security')
/* global WIKI */
module.exports = {
strategies: {},
guest: {
cacheExpiration: moment.utc().subtract(1, 'd')
},
groups: {},
/**
* Initialize the authentication module
*/
init() {
this.passport = passport
passport.serializeUser((user, done) => {
done(null, user.id)
})
passport.deserializeUser(async (id, done) => {
try {
const user = await WIKI.models.users.query().findById(id).modifyEager('groups', builder => {
builder.select('groups.id', 'permissions')
})
if (user) {
done(null, user)
} else {
done(new Error(WIKI.lang.t('auth:errors:usernotfound')), null)
}
} catch (err) {
done(err, null)
}
})
this.reloadGroups()
return this
},
/**
* Load authentication strategies
*/
async activateStrategies() {
try {
// Unload any active strategies
WIKI.auth.strategies = {}
const currentStrategies = _.keys(passport._strategies)
_.pull(currentStrategies, 'session')
_.forEach(currentStrategies, stg => { passport.unuse(stg) })
// Load JWT
passport.use('jwt', new passportJWT.Strategy({
jwtFromRequest: securityHelper.extractJWT,
secretOrKey: WIKI.config.certs.public,
audience: WIKI.config.auth.audience,
issuer: 'urn:wiki.js'
}, (jwtPayload, cb) => {
cb(null, jwtPayload)
}))
// Load enabled strategies
const enabledStrategies = await WIKI.models.authentication.getStrategies()
for (let idx in enabledStrategies) {
const stg = enabledStrategies[idx]
if (!stg.isEnabled) { continue }
const strategy = require(`../modules/authentication/${stg.key}/authentication.js`)
stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback`
strategy.init(passport, stg.config)
try {
strategy.icon = await fs.readFile(path.join(WIKI.ROOTPATH, `assets/svg/auth-icon-${strategy.key}.svg`), 'utf8')
} catch (err) {
if (err.code === 'ENOENT') {
strategy.icon = '[missing icon]'
} else {
WIKI.logger.warn(err)
}
}
WIKI.auth.strategies[stg.key] = strategy
WIKI.logger.info(`Authentication Strategy ${stg.key}: [ OK ]`)
}
} catch (err) {
WIKI.logger.error(`Authentication Strategy: [ FAILED ]`)
WIKI.logger.error(err)
}
},
/**
* Authenticate current request
*
* @param {Express Request} req
* @param {Express Response} res
* @param {Express Next Callback} next
*/
authenticate(req, res, next) {
WIKI.auth.passport.authenticate('jwt', {session: false}, async (err, user, info) => {
if (err) { return next() }
// Expired but still valid within N days, just renew
if (info instanceof Error && info.name === 'TokenExpiredError' && moment().subtract(14, 'days').isBefore(info.expiredAt)) {
const jwtPayload = jwt.decode(securityHelper.extractJWT(req))
try {
const newToken = await WIKI.models.users.refreshToken(jwtPayload.id)
user = newToken.user
user.permissions = user.getGlobalPermissions()
req.user = user
// Try headers, otherwise cookies for response
if (req.get('content-type') === 'application/json') {
res.set('new-jwt', newToken.token)
} else {
res.cookie('jwt', newToken.token, { expires: moment().add(365, 'days').toDate() })
}
} catch (err) {
WIKI.logger.warn(err)
return next()
}
}
// JWT is NOT valid, set as guest
if (!user) {
if (WIKI.auth.guest.cacheExpiration.isSameOrBefore(moment.utc())) {
WIKI.auth.guest = await WIKI.models.users.getGuestUser()
WIKI.auth.guest.cacheExpiration = moment.utc().add(1, 'm')
}
req.user = WIKI.auth.guest
return next()
}
// JWT is valid
req.logIn(user, { session: false }, (err) => {
if (err) { return next(err) }
next()
})
})(req, res, next)
},
/**
* Check if user has access to resource
*
* @param {User} user
* @param {Array<String>} permissions
* @param {String|Boolean} path
*/
checkAccess(user, permissions = [], page = false) {
const userPermissions = user.permissions ? user.permissions : user.getGlobalPermissions()
// System Admin
if (_.includes(userPermissions, 'manage:system')) {
return true
}
// Check Global Permissions
if (_.intersection(userPermissions, permissions).length < 1) {
return false
}
// Check Page Rules
if (path && user.groups) {
let checkState = {
deny: false,
match: false,
specificity: ''
}
user.groups.forEach(grp => {
const grpId = _.isObject(grp) ? _.get(grp, 'id', 0) : grp
_.get(WIKI.auth.groups, `${grpId}.pageRules`, []).forEach(rule => {
switch (rule.match) {
case 'START':
if (_.startsWith(`/${page.path}`, `/${rule.path}`)) {
checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['END', 'REGEX', 'EXACT'] })
}
break
case 'END':
if (_.endsWith(page.path, rule.path)) {
checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['REGEX', 'EXACT'] })
}
break
case 'REGEX':
const reg = new RegExp(rule.path)
if (reg.test(page.path)) {
checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['EXACT'] })
}
break
case 'EXACT':
if (`/${page.path}` === `/${rule.path}`) {
checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: [] })
}
break
}
})
})
return (checkState.match && !checkState.deny)
}
return false
},
/**
* Check and apply Page Rule specificity
*
* @access private
*/
_applyPageRuleSpecificity ({ rule, checkState, higherPriority = [] }) {
if (rule.path.length === checkState.specificity.length) {
// Do not override higher priority rules
if (_.includes(higherPriority, checkState.match)) {
return checkState
}
// Do not override a previous DENY rule with same match
if (rule.match === checkState.match && checkState.deny && !rule.deny) {
return checkState
}
} else if (rule.path.length < checkState.specificity.length) {
// Do not override higher specificity rules
return checkState
}
return {
deny: rule.deny,
match: rule.match,
specificity: rule.path
}
},
/**
* Reload Groups from DB
*/
async reloadGroups() {
const groupsArray = await WIKI.models.groups.query()
this.groups = _.keyBy(groupsArray, 'id')
}
}