Forward Auth refactor and adding PROXY_FORWARD_AUTH_LOGOUT_URI env

This commit is contained in:
advplyr 2022-12-18 11:37:45 -06:00
parent 8558baf30a
commit 73a61872fa
9 changed files with 139 additions and 68 deletions

View file

@ -1,10 +1,12 @@
const bcrypt = require('./libs/bcryptjs')
const jwt = require('./libs/jsonwebtoken')
const requestIp = require('./libs/requestIp')
const Logger = require('./Logger')
const SocketAuthority = require('./SocketAuthority')
const User = require('./objects/user/User')
const { getId } = require('./utils/index')
const { parseBool } = require('./utils/parseBool')
class Auth {
constructor(db) {
@ -97,6 +99,10 @@ class Auth {
})
}
generateRandomPasswordHash() {
return hashPass(getId())
}
generateAccessToken(payload) {
return jwt.sign(payload, global.ServerSettings.tokenSecret);
}
@ -180,59 +186,83 @@ class Auth {
* Checks for an environment variable of PROXY_FORWARD_AUTH_ENABLED and if enabled
* will authenticate the user from the proxy headers.
* @param {*} req
* @param {*} res
* @param {*} feeds
* @returns
* @param {Array} feeds
* @returns {Promise<(Object|false)>} User login response payload or false
*/
async getUserFromProxyAuth(req, res, feeds) {
var username = null;
var user = null;
// Check if $PROXY_FORWARD_AUTH_ENABLED is defined
if (!parseBool(process.env.PROXY_FORWARD_AUTH_ENABLED)) {
Logger.debug("$PROXY_FORWARD_AUTH_ENABLED not set.");
// Respond with disabled: true - client will check for it and ignore forward auth
return res.json({ disabled: true });
async getUserFromProxyAuth(req, feeds) {
if (!global.ForwardAuth.Enabled) {
return false
}
// Check that the proxy username header is set
if (!process.env.PROXY_FORWARD_AUTH_USERNAME) {
Logger.info("$PROXY_FORWARD_AUTH_ENABLED is set, but $PROXY_FORWARD_AUTH_USERNAME has not been defined. Forward Authentication will not proceed.");
return res.json({ disabled: true });
let username = req.headers[global.ForwardAuth.UsernameHeader]
if (!username) {
Logger.warn(`[Auth] Forward Auth username header ${global.ForwardAuth.UsernameHeader} has no username`)
return false
}
Logger.debug('Found $PROXY_FORWARD_AUTH_USERNAME header:', process.env.PROXY_FORWARD_AUTH_USERNAME);
username = req.headers[process.env.PROXY_FORWARD_AUTH_USERNAME.toLowerCase()];
Logger.debug('Found Forwarded Username ', username);
if (!username)
return res.status(401).send(`Username not found in headers (Header: ${process.env.PROXY_FORWARD_AUTH_USERNAME}`);
var user = this.users.find(u => u.username.toLowerCase() === username)
let user = this.users.find(u => u.username.toLowerCase() === username)
// If the user doesn't exist and PROXY_FORWARD AUTH_CREATE is enabled, create the user
if (!user && parseBool(process.env.PROXY_FORWARD_AUTH_CREATE)) {
Logger.debug('User not found, creating user', username);
// TODO - user type:
// this could come from the proxy (eg a user attribute), or could be configurable from an environment variable
var account = {
if (!user && global.ForwardAuth.CreateUser) {
Logger.debug(`[Auth] Forward Auth User not found with username "${username}" - creating it`)
const newUserData = {
id: getId('usr'),
username: username,
username,
pash: this.generateRandomPasswordHash(), // Random password will need to be reset by admin if wanting to use regular login
createdAt: Date.now(),
type: 'user'
};
account.token = await this.generateAccessToken({ userId: account.id, username: account.username });
user = new User(account);
var success = await this.db.insertEntity('user', user);
if (!success)
return res.status(500).send("Could not create user.");
}
newUserData.token = await this.generateAccessToken({ userId: newUserData.id, username: newUserData.username })
user = new User(newUserData)
const success = await this.db.insertEntity('user', user)
if (!success) {
Logger.error(`[Auth] Forward Auth failed to insert new user in DB`, user.toJSON())
return false
}
SocketAuthority.adminEmitter('user_added', user)
}
if (!user)
return res.status(401).send(`User "${username}" not found`);
if (!user) {
Logger.error(`[Auth] Forward Auth user not found with username "${username}"`)
return false
}
return res.json(this.getUserLoginResponsePayload(user, feeds))
user.isForwardAuth = true
Logger.debug(`[Auth] Forward Auth success for username "${username}"`)
return this.getUserLoginResponsePayload(user, feeds)
}
/**
* Checks if the user in req.user is valid with forward auth headers.
* @param {*} req
* @returns {boolean}
*/
validateForwardAuthUser(req) {
if (!global.ForwardAuth.Enabled) {
return false
}
let username = req.headers[global.ForwardAuth.UsernameHeader]
if (!username) {
Logger.error(`[Auth] validate: Forward Auth username header ${global.ForwardAuth.UsernameHeader} has no username`)
return false
}
// Mismatch username in header with username from token
if (req.user.username.toLowerCase() !== username) {
Logger.error(`[Auth] validate: Forward Auth username "${username}" from header ${global.ForwardAuth.UsernameHeader} does not match username "${req.user.username.toLowerCase()}" from token`)
return false
}
req.user.isForwardAuth = true
return true
}
comparePassword(password, user) {

View file

@ -10,6 +10,7 @@ const { version } = require('../package.json')
// Utils
const dbMigration = require('./utils/dbMigration')
const filePerms = require('./utils/filePerms')
const { parseBool } = require('./utils/parseBool')
const Logger = require('./Logger')
const Auth = require('./Auth')
@ -45,6 +46,12 @@ class Server {
global.ConfigPath = Path.normalize(CONFIG_PATH)
global.MetadataPath = Path.normalize(METADATA_PATH)
global.RouterBasePath = ROUTER_BASE_PATH
global.ForwardAuth = {
Enabled: parseBool(process.env.PROXY_FORWARD_AUTH_ENABLED) && process.env.PROXY_FORWARD_AUTH_USERNAME,
UsernameHeader: (process.env.PROXY_FORWARD_AUTH_USERNAME || '').toLowerCase(),
CreateUser: parseBool(process.env.PROXY_FORWARD_AUTH_CREATE),
LogoutURI: process.env.PROXY_FORWARD_AUTH_LOGOUT_URI
}
// Fix backslash if not on Windows
if (process.platform !== 'win32') {
@ -221,7 +228,7 @@ class Server {
}
this.initializeServer(req, res)
})
router.get('/status', (req, res) => {
router.get('/status', async (req, res) => {
// status check for client to see if server has been initialized
// server has been initialized if a root user exists
const payload = {
@ -231,7 +238,14 @@ class Server {
if (!payload.isInit) {
payload.ConfigPath = global.ConfigPath
payload.MetadataPath = global.MetadataPath
return res.json(payload)
}
// Check forward auth for user
if (global.ForwardAuth.Enabled) {
payload.forwardAuthUserResponse = await this.auth.getUserFromProxyAuth(req, this.rssFeedManager.feedsArray)
}
res.json(payload)
})
router.get('/ping', (req, res) => {

View file

@ -120,6 +120,13 @@ class MiscController {
Logger.error('Invalid user in authorize')
return res.sendStatus(401)
}
// When authorizing an API token and forward auth is enabled then validate user using forward auth headers
if (global.ForwardAuth.Enabled && !this.auth.validateForwardAuthUser(req)) {
Logger.error(`[MiscController] Authorize token with forward auth enabled failed for user "${req.user.username}"`)
return res.sendStatus(403)
}
const userResponse = this.auth.getUserLoginResponsePayload(req.user, this.rssFeedManager.feedsArray)
res.json(userResponse)
}

View file

@ -23,6 +23,9 @@ class User {
this.librariesAccessible = [] // Library IDs (Empty if ALL libraries)
this.itemTagsAccessible = [] // Empty if ALL item tags accessible
// Temporary values not persisted to the Db
this.isForwardAuth = false
if (user) {
this.construct(user)
}
@ -102,7 +105,7 @@ class User {
}
toJSONForBrowser() {
return {
const json = {
id: this.id,
username: this.username,
type: this.type,
@ -117,8 +120,15 @@ class User {
settings: this.settings, // TODO: Remove after mobile release v0.9.61-beta
permissions: this.permissions,
librariesAccessible: [...this.librariesAccessible],
itemTagsAccessible: [...this.itemTagsAccessible]
itemTagsAccessible: [...this.itemTagsAccessible],
isForwardAuth: this.isForwardAuth
}
if (this.isForwardAuth) {
json.forwardAuthLogoutURI = global.ForwardAuth.LogoutURI || null
}
return json
}
// Data broadcasted

View file

@ -1,8 +1,8 @@
const true_values = ["true", "yes", "y", "1", "on"];
const true_values = ["true", "yes", "y", "1", "on"]
module.exports.parseBool = (value) => {
if (Object.is(value, null) || value === undefined) {
return false;
return false
}
return true_values.includes(String(value).toLowerCase());
return true_values.includes(String(value).toLowerCase())
}