From 49c1b8f106bb9c6f4c1347062b5d8e61c43445bb Mon Sep 17 00:00:00 2001 From: Igor Serebryany Date: Fri, 6 Oct 2023 14:04:31 -0700 Subject: [PATCH] enable log in via either token or through proxy auth --- server/Auth.js | 95 ++++++++++++++++++++--- server/controllers/UserController.js | 28 +++---- server/objects/settings/ServerSettings.js | 13 ++++ 3 files changed, 108 insertions(+), 28 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 6c7b98914..eb0927b39 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -1,8 +1,11 @@ +const uuidv4 = require("uuid").v4 const bcrypt = require('./libs/bcryptjs') const jwt = require('./libs/jsonwebtoken') const requestIp = require('./libs/requestIp') + const Logger = require('./Logger') const Database = require('./Database') +const User = require('./objects/user/User') class Auth { constructor() { } @@ -53,16 +56,46 @@ class Auth { token = authHeader && authHeader.split(' ')[1] } - if (token == null) { - Logger.error('Api called without a token', req.path) + let proxyUsername = null + if (Database.serverSettings.proxyAuthEnabled) { + const uHeader = Database.serverSettings.proxyAuthUsernameHeader.toLowerCase() + proxyUsername = uHeader ? req.headers[uHeader] : null + } + + if (token == null && proxyUsername == null) { + Logger.error('Api called without a token and proxy auth is disabled', req.path) return res.sendStatus(401) } - const user = await this.verifyToken(token) - if (!user) { - Logger.error('Verify Token User Not Found', token) - return res.sendStatus(404) + let user + if (token) { + user = await this.verifyToken(token) + if (!user) { + Logger.error('Verify Token User Not Found', token) + return res.sendStatus(401) + } + + user.isFromProxy = false + } else if (proxyUsername) { + const eHeader = Database.serverSettings.proxyAuthEmailHeader.toLowerCase() + const proxyEmail = eHeader ? req.headers[eHeader] : null + + user = await this.getProxyUser(proxyUsername, proxyEmail) + if (!user) { + Logger.error('Cannot get user from proxy headers', proxyUsername) + return res.sendStatus(401) + } + + user.isFromProxy = true } + + // if we got here, we should have a valid user in `user` + if (!user) { + Logger.error('Unknown error getting user') + return res.sendStatus(401) + } + + // is the user active if (!user.isActive) { Logger.error('Verify Token User is disabled', token, user.username) return res.sendStatus(403) @@ -110,9 +143,52 @@ class Auth { }) } + async getProxyUser(username, email) { + const user = await Database.userModel.getUserByUsername(username) + if (user) { + return user + } + + if (!email) { + return null + } + + const account = { + username, + email, + type: 'user', + isActive: true, + } + + return await this.createUser(account) + } + + // create and return a new user from an object with lots of user properties defined + async createUser(account) { + const username = account.username + const usernameExists = await Database.userModel.getUserByUsername(username) + if (usernameExists) { + return null + } + + account.id = uuidv4() + + const password = account.password || uuidv4() + delete account.password + + account.pash = await this.hashPass(password) + + account.token = await this.generateAccessToken({ userId: account.id, username }) + account.createdAt = Date.now() + const newUser = new User(account) + + const success = await Database.createUser(newUser) + return success ? await Database.userModel.getUserById(newUser.id) : null + } + /** * Payload returned to a user after successful login - * @param {oldUser} user + * @param {oldUser} user * @returns {object} */ async getUserLoginResponsePayload(user) { @@ -187,8 +263,9 @@ class Auth { }) } + // proxy users can change their password without knowing their old password const compare = await this.comparePassword(password, matchingUser) - if (!compare) { + if (!compare && !req.user.isFromProxy) { return res.json({ error: 'Invalid password' }) @@ -218,4 +295,4 @@ class Auth { } } } -module.exports = Auth \ No newline at end of file +module.exports = Auth diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index 2695a7a0f..7888d31bf 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -1,10 +1,7 @@ -const uuidv4 = require("uuid").v4 const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') -const User = require('../objects/user/User') - const { toNumber } = require('../utils/index') class UserController { @@ -36,9 +33,9 @@ class UserController { * GET: /api/users/:id * Get a single user toJSONForBrowser * Media progress items include: `displayTitle`, `displaySubtitle` (for podcasts), `coverPath` and `mediaUpdatedAt` - * - * @param {import("express").Request} req - * @param {import("express").Response} res + * + * @param {import("express").Request} req + * @param {import("express").Response} res */ async findOne(req, res) { if (!req.user.isAdminOrUp) { @@ -97,15 +94,8 @@ class UserController { return res.status(500).send('Username already taken') } - account.id = uuidv4() - account.pash = await this.auth.hashPass(account.password) - delete account.password - account.token = await this.auth.generateAccessToken({ userId: account.id, username }) - account.createdAt = Date.now() - const newUser = new User(account) - - const success = await Database.createUser(newUser) - if (success) { + const newUser = await this.auth.createUser(account) + if (newUser) { SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser()) res.json({ user: newUser.toJSONForBrowser() @@ -118,9 +108,9 @@ class UserController { /** * PATCH: /api/users/:id * Update user - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async update(req, res) { const user = req.reqUser @@ -250,4 +240,4 @@ class UserController { next() } } -module.exports = new UserController() \ No newline at end of file +module.exports = new UserController() diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 418b80540..80b335e27 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -51,6 +51,11 @@ class ServerSettings { this.timeFormat = 'HH:mm' this.language = 'en-us' + // Reverse Proxy + this.proxyAuthEnabled = false + this.proxyAuthUsernameHeader = '' + this.proxyAuthEmailHeader = '' + this.logLevel = Logger.logLevel this.version = null @@ -94,6 +99,11 @@ class ServerSettings { this.dateFormat = settings.dateFormat || 'MM/dd/yyyy' this.timeFormat = settings.timeFormat || 'HH:mm' this.language = settings.language || 'en-us' + + this.proxyAuthEnabled = !!settings.proxyAuthEnabled + this.proxyAuthUsernameHeader = settings.proxyAuthUsernameHeader || '' + this.proxyAuthEmailHeader = settings.proxyAuthEmailHeader || '' + this.logLevel = settings.logLevel || Logger.logLevel this.version = settings.version || null @@ -151,6 +161,9 @@ class ServerSettings { sortingIgnorePrefix: this.sortingIgnorePrefix, sortingPrefixes: [...this.sortingPrefixes], chromecastEnabled: this.chromecastEnabled, + proxyAuthEnabled: this.proxyAuthEnabled, + proxyAuthUsernameHeader: this.proxyAuthUsernameHeader, + proxyAuthEmailHeader: this.proxyAuthEmailHeader, dateFormat: this.dateFormat, timeFormat: this.timeFormat, language: this.language,