From 9e913b5e17129cb742e36888debe612be3beac0f Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 7 Feb 2022 17:52:50 -0600 Subject: [PATCH] Update passport init with saved settings & check if OIDC is configured --- server/Auth.js | 33 +++++++++++++- server/Server.js | 83 +++++++++++------------------------ server/objects/SSOSettings.js | 62 ++++++++++++++------------ server/objects/User.js | 16 +++---- 4 files changed, 97 insertions(+), 97 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 8ed8209f0..4a9a6d2a7 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -1,7 +1,7 @@ const bcrypt = require('bcryptjs') const jwt = require('jsonwebtoken') -const passport = require('passport') const Logger = require('./Logger') +const User = require('./objects/User') class Auth { constructor(db) { @@ -49,7 +49,7 @@ class Auth { } req.user = user - + return next(); } @@ -210,5 +210,34 @@ class Auth { }) } } + + async handleOIDCVerification(issuer, profile, cb) { + Logger.debug(`[Auth] handleOIDCVerification ${issuer}`) + + let user = this.db.users.find(u => u.id === profile.id) + if (!user && this.db.SSOSettings.createNewUser) { + // create a user + let account = {} + account.id = profile.id + account.username = profile.username + account.isActive = true + account.type = "guest" + account.permissions = this.db.SSOSettings.getNewUserPermissions() + account.pash = await this.hashPass(getId(profile.id)) + account.token = await this.generateAccessToken({ userId: account.id }) + account.createdAt = Date.now() + user = new User(account) + const success = await this.db.insertEntity('user', user) + if (!success) { + cb('Failed to save new user') + } + } + if (!user || !user.isActive) { + Logger.debug(`[Auth] Failed login attempt`) + cb("Invalid user or password") + return + } + cb(null, user) + } } module.exports = Auth \ No newline at end of file diff --git a/server/Server.js b/server/Server.js index 6d3a4d6f1..99e4d3c7e 100644 --- a/server/Server.js +++ b/server/Server.js @@ -7,8 +7,8 @@ const SocketIO = require('socket.io') const fs = require('fs-extra') const fileUpload = require('express-fileupload') const rateLimit = require('express-rate-limit') -const passport = require('passport'); -const OidcStrategy = require('passport-openidconnect').Strategy; +const passport = require('passport') +const OidcStrategy = require('passport-openidconnect').Strategy const { version } = require('../package.json') @@ -32,7 +32,6 @@ const RssFeeds = require('./RssFeeds') const DownloadManager = require('./DownloadManager') const CoverController = require('./CoverController') const CacheManager = require('./CacheManager') -const User = require('./objects/User') class Server { constructor(PORT, UID, GID, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) { @@ -44,10 +43,6 @@ class Server { this.AudiobookPath = Path.normalize(AUDIOBOOK_PATH) this.MetadataPath = Path.normalize(METADATA_PATH) - console.info(this.ConfigPath) - console.info(this.MetadataPath) - console.info(this.AudiobookPath) - fs.ensureDirSync(CONFIG_PATH, 0o774) fs.ensureDirSync(METADATA_PATH, 0o774) fs.ensureDirSync(AUDIOBOOK_PATH, 0o774) @@ -74,54 +69,6 @@ class Server { this.io = null this.clients = {} - passport.serializeUser((user, next) => { - next(null, user); - }); - - passport.deserializeUser((obj, next) => { - next(null, obj); - }); - passport.use(new OidcStrategy({ - issuer: process.env.OIDC_ISSUER, - authorizationURL: process.env.OIDC_AUTHORIZATION_URL, - tokenURL: process.env.OIDC_TOKEN_URL, - userInfoURL: process.env.OIDC_USER_INFO_URL, - clientID: process.env.OIDC_CLIENT_ID, - clientSecret: process.env.OIDC_CLIENT_SECRET, - callbackURL: '/oidc/callback', - scope: "openid email profile" - }, async (issuer, profile, cb) => { - let user = this.db.users.find(u => u.id === profile.id) - if (!user) { - // create a user - let account = {} - account.id = profile.id - account.username = profile.username - account.type = "guest" - account.permissions = { - download: false, - update: false, - delete: false, - upload: false, - accessAllLibraries: false - } - account.pash = await this.auth.hashPass(getId(profile.id)) - account.token = await this.auth.generateAccessToken({ userId: account.id }) - account.createdAt = Date.now() - user = new User(account) - const success = await this.db.insertEntity('user', user) - if (!success) { - cb('Failed to save new user') - } - } - if (!user || !user.isActive) { - Logger.debug(`[Auth] Failed login attempt`) - cb("Invalid user or password") - return - } - cb(null, user) - }) - ) } get audiobooks() { @@ -186,6 +133,26 @@ class Server { this.watcher.initWatcher(this.libraries) this.watcher.on('files', this.filesChanged.bind(this)) + + this.passportInit() + } + + passportInit() { + if (this.db.SSOSettings.isOIDCConfigured) { + Logger.debug(`[Server] passportInit OIDC is configured - init`) + + passport.serializeUser((user, next) => { + next(null, user); + }) + passport.deserializeUser((obj, next) => { + next(null, obj); + }) + + // Initialize passport OIDC verification + passport.use(new OidcStrategy(this.db.SSOSettings.getOIDCSettings(), this.auth.handleOIDCVerification)) + } else { + Logger.debug(`[Server] passportInit OIDC not configured`) + } } async start() { @@ -218,7 +185,7 @@ class Server { token = authHeader && authHeader.split(' ')[1] } */ - + // Static path to generated nuxt const distPath = Path.join(global.appRoot, '/client/dist') app.use(express.static(distPath)) @@ -295,10 +262,10 @@ class Server { app.get("/oidc/login", passport.authenticate('openidconnect')) - app.get("/oidc/callback", + app.get("/oidc/callback", passport.authenticate('openidconnect', { failureRedirect: '/oidc/login', failureMessage: true }), async (req, res) => { - const token = this.auth.generateAccessToken({userId: req.user.id}) + const token = this.auth.generateAccessToken({ userId: req.user.id }) res.cookie('token', token, { httpOnly: true /* TODO: Set secure: true */ }); res.cookie('sso', true, { httpOnly: false /* TODO: Set secure: true */ }); diff --git a/server/objects/SSOSettings.js b/server/objects/SSOSettings.js index ba5c44b64..76cc58c72 100644 --- a/server/objects/SSOSettings.js +++ b/server/objects/SSOSettings.js @@ -1,6 +1,6 @@ -const { CoverDestination, BookCoverAspectRatio, BookshelfView } = require('../utils/constants') const Logger = require('../Logger') const User = require('./User') +const { isObject } = require('../utils') const defaultSettings = { oidc: { @@ -13,57 +13,61 @@ const defaultSettings = { callbackURL: "/oidc/callback", scope: "openid email profile" }, - user: { - createNewUser: false, - isActive: true, - userSettings: { - mobileOrderBy: 'recent', - mobileOrderDesc: true, - mobileFilterBy: 'all', - orderBy: 'book.title', - orderDesc: false, - filterBy: 'all', - playbackRate: 1, - bookshelfCoverSize: 120, - collapseSeries: false - }, - permissions: { - download: false, - update: false, - delete: false, - upload: false, - accessAllLibraries: false - } - } + createNewUser: false, + userPermissions: User.getDefaultUserPermissions('guest') } class SSOSettings { constructor(settings = defaultSettings) { this.id = 'sso-settings' this.oidc = { ...settings.oidc } - this.user = { ...settings.user } + this.createNewUser = !!settings.createNewUser + this.userPermissions = { ...settings.userPermissions } + } + + get isOIDCConfigured() { + // Check required OIDC settings are set + return !['issue', 'authorizationURL', 'tokenURL', 'clientID', 'clientSecret'].some(key => !this.oidc[key]) } toJSON() { return { id: this.id, oidc: { ...this.oidc }, - user: { ...this.user } + createNewUser: this.createNewUser, + userPermissions: { ...this.userPermissions } } } update(payload) { let hasUpdates = false for (const key in payload) { - for (const setting in payload) { - if (!this[key] || this[key][setting] === payload[key][setting]) { - continue + if (isObject(payload[key])) { + for (const setting in payload[key]) { + if (!this[key] || this[key][setting] === payload[key][setting]) { + continue + } + this[key][setting] = payload[key][setting] + hasUpdates = true } - this[key][setting] = payload[key][setting] + } else if (this[key] !== undefined && this[key] !== payload[key]) { + this[key] = payload[key] hasUpdates = true } } return hasUpdates } + + getNewUserPermissions() { + return { + ...this.userPermissions + } + } + + getOIDCSettings() { + return { + ...this.oidc + } + } } module.exports = SSOSettings \ No newline at end of file diff --git a/server/objects/User.js b/server/objects/User.js index 2a946fde2..ddb53b8c2 100644 --- a/server/objects/User.js +++ b/server/objects/User.js @@ -46,7 +46,7 @@ class User { return !!this.pash && !!this.pash.length } - getDefaultUserSettings() { + static getDefaultUserSettings() { return { mobileOrderBy: 'recent', mobileOrderDesc: true, @@ -60,12 +60,12 @@ class User { } } - getDefaultUserPermissions() { + static getDefaultUserPermissions(type) { return { - download: true, - update: true, - delete: this.type === 'root', - upload: this.type === 'root' || this.type === 'admin', + download: type !== 'guest', + update: type !== 'guest', + delete: type === 'root', + upload: type === 'root' || type === 'admin', accessAllLibraries: true } } @@ -152,8 +152,8 @@ class User { this.isLocked = user.type === 'root' ? false : !!user.isLocked this.lastSeen = user.lastSeen || null this.createdAt = user.createdAt || Date.now() - this.settings = user.settings || this.getDefaultUserSettings() - this.permissions = user.permissions || this.getDefaultUserPermissions() + this.settings = user.settings || User.getDefaultUserSettings() + this.permissions = user.permissions || User.getDefaultUserPermissions(this.type) // Upload permission added v1.1.13, make sure root user has upload permissions if (this.type === 'root' && !this.permissions.upload) this.permissions.upload = true