Add new API endpoint for updating auth-settings and update passport auth strategies

This commit is contained in:
advplyr 2023-11-10 16:11:51 -06:00
parent 078cb0855f
commit 237fe84c54
5 changed files with 255 additions and 119 deletions

View File

@ -199,13 +199,19 @@ export default {
if (this.enableOpenIDAuth) this.newAuthSettings.authActiveAuthMethods.push('openid')
this.savingSettings = true
const success = await this.$store.dispatch('updateServerSettings', this.newAuthSettings)
this.savingSettings = false
if (success) {
this.$axios
.$patch('/api/auth-settings', this.newAuthSettings)
.then((data) => {
this.$store.commit('setServerSettings', data.serverSettings)
this.$toast.success('Server settings updated')
} else {
})
.catch((error) => {
console.error('Failed to update server settings', error)
this.$toast.error('Failed to update server settings')
}
})
.finally(() => {
this.savingSettings = false
})
},
init() {
this.newAuthSettings = {

View File

@ -36,7 +36,12 @@ class Auth {
async initPassportJs() {
// Check if we should load the local strategy (username + password login)
if (global.ServerSettings.authActiveAuthMethods.includes("local")) {
passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this)))
this.initAuthStrategyPassword()
}
// Check if we should load the openid strategy
if (global.ServerSettings.authActiveAuthMethods.includes("openid")) {
this.initAuthStrategyOpenID()
}
// Check if we should load the google-oauth20 strategy
@ -62,8 +67,44 @@ class Auth {
}).bind(this)))
}
// Check if we should load the openid strategy
if (global.ServerSettings.authActiveAuthMethods.includes("openid")) {
// Load the JwtStrategy (always) -> for bearer token auth
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]),
secretOrKey: Database.serverSettings.tokenSecret
}, this.jwtAuthCheck.bind(this)))
// define how to seralize a user (to be put into the session)
passport.serializeUser(function (user, cb) {
process.nextTick(function () {
// only store id to session
return cb(null, JSON.stringify({
id: user.id,
}))
})
})
// define how to deseralize a user (use the ID to get it from the database)
passport.deserializeUser((function (user, cb) {
process.nextTick((async function () {
const parsedUserInfo = JSON.parse(user)
// load the user by ID that is stored in the session
const dbUser = await Database.userModel.getUserById(parsedUserInfo.id)
return cb(null, dbUser)
}).bind(this))
}).bind(this))
}
/**
* Passport use LocalStrategy
*/
initAuthStrategyPassword() {
passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this)))
}
/**
* Passport use OpenIDClient.Strategy
*/
initAuthStrategyOpenID() {
const openIdIssuerClient = new OpenIDClient.Issuer({
issuer: global.ServerSettings.authOpenIDIssuerURL,
authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL,
@ -140,31 +181,28 @@ class Auth {
}))
}
// Load the JwtStrategy (always) -> for bearer token auth
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]),
secretOrKey: Database.serverSettings.tokenSecret
}, this.jwtAuthCheck.bind(this)))
/**
* Unuse strategy
*
* @param {string} name
*/
unuseAuthStrategy(name) {
passport.unuse(name)
}
// define how to seralize a user (to be put into the session)
passport.serializeUser(function (user, cb) {
process.nextTick(function () {
// only store id to session
return cb(null, JSON.stringify({
id: user.id,
}))
})
})
// define how to deseralize a user (use the ID to get it from the database)
passport.deserializeUser((function (user, cb) {
process.nextTick((async function () {
const parsedUserInfo = JSON.parse(user)
// load the user by ID that is stored in the session
const dbUser = await Database.userModel.getUserById(parsedUserInfo.id)
return cb(null, dbUser)
}).bind(this))
}).bind(this))
/**
* Use strategy
*
* @param {string} name
*/
useAuthStrategy(name) {
if (name === 'openid') {
this.initAuthStrategyOpenID()
} else if (name === 'local') {
this.initAuthStrategyPassword()
} else {
Logger.error('[Auth] Invalid auth strategy ' + name)
}
}
/**

View File

@ -129,7 +129,7 @@ class MiscController {
return res.sendStatus(403)
}
const settingsUpdate = req.body
if (!settingsUpdate || !isObject(settingsUpdate)) {
if (!isObject(settingsUpdate)) {
return res.status(400).send('Invalid settings update object')
}
@ -604,5 +604,91 @@ class MiscController {
}
return res.json(Database.serverSettings.authenticationSettings)
}
/**
* PATCH: api/auth-settings
* @this import('../routers/ApiRouter')
*
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
async updateAuthSettings(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to update auth settings`)
return res.sendStatus(403)
}
const settingsUpdate = req.body
if (!isObject(settingsUpdate)) {
return res.status(400).send('Invalid auth settings update object')
}
let hasUpdates = false
const currentAuthenticationSettings = Database.serverSettings.authenticationSettings
const originalAuthMethods = [...currentAuthenticationSettings.authActiveAuthMethods]
// TODO: Better validation of auth settings once auth settings are separated from server settings
for (const key in currentAuthenticationSettings) {
if (settingsUpdate[key] === undefined) continue
if (key === 'authActiveAuthMethods') {
let updatedAuthMethods = settingsUpdate[key]?.filter?.((authMeth) => Database.serverSettings.supportedAuthMethods.includes(authMeth))
if (Array.isArray(updatedAuthMethods) && updatedAuthMethods.length) {
updatedAuthMethods.sort()
currentAuthenticationSettings[key].sort()
if (updatedAuthMethods.join() !== currentAuthenticationSettings[key].join()) {
Logger.debug(`[MiscController] Updating auth settings key "authActiveAuthMethods" from "${currentAuthenticationSettings[key].join()}" to "${updatedAuthMethods.join()}"`)
Database.serverSettings[key] = updatedAuthMethods
hasUpdates = true
}
} else {
Logger.warn(`[MiscController] Invalid value for authActiveAuthMethods`)
}
} else {
const updatedValueType = typeof settingsUpdate[key]
if (['authOpenIDAutoLaunch', 'authOpenIDAutoRegister'].includes(key)) {
if (updatedValueType !== 'boolean') {
Logger.warn(`[MiscController] Invalid value for ${key}. Expected boolean`)
continue
}
} else if (updatedValueType !== null && updatedValueType !== 'string') {
Logger.warn(`[MiscController] Invalid value for ${key}. Expected string or null`)
continue
}
let updatedValue = settingsUpdate[key]
if (updatedValue === '') updatedValue = null
let currentValue = currentAuthenticationSettings[key]
if (currentValue === '') currentValue = null
if (updatedValue !== currentValue) {
Logger.debug(`[MiscController] Updating auth settings key "${key}" from "${currentValue}" to "${updatedValue}"`)
Database.serverSettings[key] = updatedValue
hasUpdates = true
}
}
}
if (hasUpdates) {
// Use/unuse auth methods
Database.serverSettings.supportedAuthMethods.forEach((authMethod) => {
if (originalAuthMethods.includes(authMethod) && !Database.serverSettings.authActiveAuthMethods.includes(authMethod)) {
// Auth method has been removed
Logger.info(`[MiscController] Disabling active auth method "${authMethod}"`)
this.auth.unuseAuthStrategy(authMethod)
} else if (!originalAuthMethods.includes(authMethod) && Database.serverSettings.authActiveAuthMethods.includes(authMethod)) {
// Auth method has been added
Logger.info(`[MiscController] Enabling active auth method "${authMethod}"`)
this.auth.useAuthStrategy(authMethod)
}
})
await Database.updateServerSettings()
}
res.json({
serverSettings: Database.serverSettings.toJSONForBrowser()
})
}
}
module.exports = new MiscController()

View File

@ -59,19 +59,19 @@ class ServerSettings {
this.authActiveAuthMethods = ['local']
// google-oauth20 settings
this.authGoogleOauth20ClientID = ''
this.authGoogleOauth20ClientSecret = ''
this.authGoogleOauth20CallbackURL = ''
this.authGoogleOauth20ClientID = null
this.authGoogleOauth20ClientSecret = null
this.authGoogleOauth20CallbackURL = null
// openid settings
this.authOpenIDIssuerURL = ''
this.authOpenIDAuthorizationURL = ''
this.authOpenIDTokenURL = ''
this.authOpenIDUserInfoURL = ''
this.authOpenIDJwksURL = ''
this.authOpenIDLogoutURL = ''
this.authOpenIDClientID = ''
this.authOpenIDClientSecret = ''
this.authOpenIDIssuerURL = null
this.authOpenIDAuthorizationURL = null
this.authOpenIDTokenURL = null
this.authOpenIDUserInfoURL = null
this.authOpenIDJwksURL = null
this.authOpenIDLogoutURL = null
this.authOpenIDClientID = null
this.authOpenIDClientSecret = null
this.authOpenIDButtonText = 'Login with OpenId'
this.authOpenIDAutoLaunch = false
this.authOpenIDAutoRegister = false
@ -118,18 +118,18 @@ class ServerSettings {
this.buildNumber = settings.buildNumber || 0 // Added v2.4.5
this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local']
this.authGoogleOauth20ClientID = settings.authGoogleOauth20ClientID || ''
this.authGoogleOauth20ClientSecret = settings.authGoogleOauth20ClientSecret || ''
this.authGoogleOauth20CallbackURL = settings.authGoogleOauth20CallbackURL || ''
this.authGoogleOauth20ClientID = settings.authGoogleOauth20ClientID || null
this.authGoogleOauth20ClientSecret = settings.authGoogleOauth20ClientSecret || null
this.authGoogleOauth20CallbackURL = settings.authGoogleOauth20CallbackURL || null
this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || ''
this.authOpenIDAuthorizationURL = settings.authOpenIDAuthorizationURL || ''
this.authOpenIDTokenURL = settings.authOpenIDTokenURL || ''
this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || ''
this.authOpenIDJwksURL = settings.authOpenIDJwksURL || ''
this.authOpenIDLogoutURL = settings.authOpenIDLogoutURL || ''
this.authOpenIDClientID = settings.authOpenIDClientID || ''
this.authOpenIDClientSecret = settings.authOpenIDClientSecret || ''
this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null
this.authOpenIDAuthorizationURL = settings.authOpenIDAuthorizationURL || null
this.authOpenIDTokenURL = settings.authOpenIDTokenURL || null
this.authOpenIDUserInfoURL = settings.authOpenIDUserInfoURL || null
this.authOpenIDJwksURL = settings.authOpenIDJwksURL || null
this.authOpenIDLogoutURL = settings.authOpenIDLogoutURL || null
this.authOpenIDClientID = settings.authOpenIDClientID || null
this.authOpenIDClientSecret = settings.authOpenIDClientSecret || null
this.authOpenIDButtonText = settings.authOpenIDButtonText || 'Login with OpenId'
this.authOpenIDAutoLaunch = !!settings.authOpenIDAutoLaunch
this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister
@ -142,9 +142,9 @@ class ServerSettings {
// remove uninitialized methods
// GoogleOauth20
if (this.authActiveAuthMethods.includes('google-oauth20') && (
this.authGoogleOauth20ClientID === '' ||
this.authGoogleOauth20ClientSecret === '' ||
this.authGoogleOauth20CallbackURL === ''
!this.authGoogleOauth20ClientID ||
!this.authGoogleOauth20ClientSecret ||
!this.authGoogleOauth20CallbackURL
)) {
this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1)
}
@ -152,13 +152,13 @@ class ServerSettings {
// remove uninitialized methods
// OpenID
if (this.authActiveAuthMethods.includes('openid') && (
this.authOpenIDIssuerURL === '' ||
this.authOpenIDAuthorizationURL === '' ||
this.authOpenIDTokenURL === '' ||
this.authOpenIDUserInfoURL === '' ||
this.authOpenIDJwksURL === '' ||
this.authOpenIDClientID === '' ||
this.authOpenIDClientSecret === ''
!this.authOpenIDIssuerURL ||
!this.authOpenIDAuthorizationURL ||
!this.authOpenIDTokenURL ||
!this.authOpenIDUserInfoURL ||
!this.authOpenIDJwksURL ||
!this.authOpenIDClientID ||
!this.authOpenIDClientSecret
)) {
this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('openid', 0), 1)
}
@ -254,6 +254,10 @@ class ServerSettings {
return json
}
get supportedAuthMethods() {
return ['local', 'openid']
}
get authenticationSettings() {
return {
authActiveAuthMethods: this.authActiveAuthMethods,

View File

@ -35,6 +35,7 @@ const Series = require('../objects/entities/Series')
class ApiRouter {
constructor(Server) {
/** @type {import('../Auth')} */
this.auth = Server.auth
this.playbackSessionManager = Server.playbackSessionManager
this.abMergeManager = Server.abMergeManager
@ -310,6 +311,7 @@ class ApiRouter {
this.router.delete('/genres/:genre', MiscController.deleteGenre.bind(this))
this.router.post('/validate-cron', MiscController.validateCronExpression.bind(this))
this.router.get('/auth-settings', MiscController.getAuthSettings.bind(this))
this.router.patch('/auth-settings', MiscController.updateAuthSettings.bind(this))
this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this))
}