feat: proxy authenfication added

This commit is contained in:
alex-sviridov 2025-09-29 12:17:59 +02:00
parent 03da194953
commit 4875125ae9
6 changed files with 315 additions and 2 deletions

View file

@ -8,6 +8,7 @@ const Logger = require('./Logger')
const TokenManager = require('./auth/TokenManager')
const LocalAuthStrategy = require('./auth/LocalAuthStrategy')
const OidcAuthStrategy = require('./auth/OidcAuthStrategy')
const ProxyAuthStrategy = require('./auth/ProxyAuthStrategy')
const RateLimiterFactory = require('./utils/rateLimiterFactory')
const { escapeRegExp } = require('./utils')
@ -26,6 +27,7 @@ class Auth {
this.tokenManager = new TokenManager()
this.localAuthStrategy = new LocalAuthStrategy()
this.oidcAuthStrategy = new OidcAuthStrategy()
this.proxyAuthStrategy = new ProxyAuthStrategy()
}
/**
@ -59,6 +61,31 @@ class Auth {
* @param {NextFunction} next
*/
isAuthenticated(req, res, next) {
// If proxy auth is enabled and configured, try proxy auth first
if (global.ServerSettings.authActiveAuthMethods.includes('proxy') && global.ServerSettings.authProxyHeaderName) {
const headerName = global.ServerSettings.authProxyHeaderName
const username = req.get(headerName)
if (username) {
// Try proxy authentication first
return passport.authenticate('proxy', { session: false }, (err, user, info) => {
if (err) {
Logger.error('[Auth] Proxy authentication error:', err)
return next(err)
}
if (user) {
// Proxy auth succeeded
req.user = user
return next()
}
// Proxy auth failed, fall back to JWT
Logger.debug('[Auth] Proxy auth failed, falling back to JWT:', info?.message)
return passport.authenticate('jwt', { session: false })(req, res, next)
})(req, res, next)
}
}
// No proxy auth or no header present, use JWT authentication
return passport.authenticate('jwt', { session: false })(req, res, next)
}
@ -119,6 +146,11 @@ class Auth {
this.oidcAuthStrategy.init()
}
// Check if we should load the proxy strategy
if (global.ServerSettings.authActiveAuthMethods.includes('proxy')) {
this.proxyAuthStrategy.init()
}
// Load the JwtStrategy (always) -> for bearer token auth
passport.use(
new JwtStrategy(
@ -171,6 +203,8 @@ class Auth {
this.oidcAuthStrategy.unuse()
} else if (name === 'local') {
this.localAuthStrategy.unuse()
} else if (name === 'proxy') {
this.proxyAuthStrategy.unuse()
} else {
Logger.error('[Auth] Invalid auth strategy ' + name)
}
@ -186,6 +220,8 @@ class Auth {
this.oidcAuthStrategy.init()
} else if (name === 'local') {
this.localAuthStrategy.init()
} else if (name === 'proxy') {
this.proxyAuthStrategy.init()
} else {
Logger.error('[Auth] Invalid auth strategy ' + name)
}
@ -325,6 +361,18 @@ class Auth {
res.json(userResponse)
})
// Proxy strategy login route (reads username from header)
router.post('/auth/proxy', this.authRateLimiter, passport.authenticate('proxy'), async (req, res) => {
// Check if mobile app wants refresh token in response
const returnTokens = req.headers['x-return-tokens'] === 'true'
// Set auth method cookie for proxy authentication
res.cookie('auth_method', 'proxy', { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true })
const userResponse = await this.handleLoginSuccess(req, res, returnTokens)
res.json(userResponse)
})
// Refresh token route
router.post('/auth/refresh', this.authRateLimiter, async (req, res) => {
let refreshToken = req.cookies.refresh_token
@ -501,6 +549,12 @@ class Auth {
if (authMethod === 'openid' || authMethod === 'openid-mobile') {
logoutUrl = this.oidcAuthStrategy.getEndSessionUrl(req, req.cookies.openid_id_token, authMethod)
res.clearCookie('openid_id_token')
} else if (authMethod === 'proxy') {
// Use configured proxy logout URL if available
if (global.ServerSettings.authProxyLogoutURL) {
logoutUrl = global.ServerSettings.authProxyLogoutURL
Logger.info(`[Auth] Redirecting proxy user to configured logout URL: ${logoutUrl}`)
}
}
// Tell the user agent (browser) to redirect to the authentification provider's logout URL

View file

@ -0,0 +1,168 @@
const passport = require('passport')
const Database = require('../Database')
const Logger = require('../Logger')
/**
* Custom strategy for proxy authentication
* Reads username from configurable header set by proxy middleware
*/
class ProxyStrategy {
constructor(verify) {
this.name = 'proxy'
this.verify = verify
}
authenticate(req, options) {
const headerName = global.ServerSettings.authProxyHeaderName
const ip = req.ip || req.connection?.remoteAddress || 'Unknown'
const method = req.method
const url = req.originalUrl || req.url
// Log all proxy auth attempts for debugging
Logger.debug(`[ProxyAuthStrategy] ${method} ${url} from IP ${ip}`)
Logger.debug(`[ProxyAuthStrategy] Configured header name: ${headerName}`)
// Log all headers for debugging (but mask sensitive ones)
const headers = {}
for (const [key, value] of Object.entries(req.headers)) {
if (key.toLowerCase().includes('authorization') || key.toLowerCase().includes('cookie')) {
headers[key] = '[MASKED]'
} else {
headers[key] = value
}
}
Logger.debug(`[ProxyAuthStrategy] Request headers:`, headers)
if (!headerName) {
Logger.warn(`[ProxyAuthStrategy] Proxy header name not configured for ${method} ${url} from IP ${ip}`)
return this.fail({ message: 'Proxy header name not configured' }, 500)
}
const username = req.get(headerName)
Logger.debug(`[ProxyAuthStrategy] Header ${headerName} value: "${username}"`)
if (!username) {
Logger.warn(`[ProxyAuthStrategy] No ${headerName} header found for ${method} ${url} from IP ${ip}`)
return this.fail({ message: `No ${headerName} header found` }, 401)
}
const verified = (err, user, info) => {
if (err) {
return this.error(err)
}
if (!user) {
return this.fail(info, 401)
}
return this.success(user, info)
}
try {
this.verify(req, username, verified)
} catch (ex) {
return this.error(ex)
}
}
}
/**
* Proxy authentication strategy using configurable header
*/
class ProxyAuthStrategy {
constructor() {
this.name = 'proxy'
this.strategy = null
}
/**
* Get the passport strategy instance
* @returns {ProxyStrategy}
*/
getStrategy() {
if (!this.strategy) {
this.strategy = new ProxyStrategy(this.verifyUser.bind(this))
}
return this.strategy
}
/**
* Initialize the strategy with passport
*/
init() {
passport.use(this.name, this.getStrategy())
}
/**
* Remove the strategy from passport
*/
unuse() {
passport.unuse(this.name)
this.strategy = null
}
/**
* Verify user from proxy header
* @param {import('express').Request} req
* @param {string} username
* @param {Function} done - Passport callback
*/
async verifyUser(req, username, done) {
try {
// Normalize username (trim and lowercase, following existing pattern)
const normalizedUsername = username.trim().toLowerCase()
if (!normalizedUsername) {
const headerName = global.ServerSettings.authProxyHeaderName
this.logFailedLoginAttempt(req, username, `Empty username in ${headerName} header`)
return done(null, false, { message: `Invalid username in ${headerName} header` })
}
// Look up user in database
let user = await Database.userModel.getUserByUsername(normalizedUsername)
if (user && !user.isActive) {
this.logFailedLoginAttempt(req, normalizedUsername, 'User is not active')
return done(null, false, { message: 'User account is disabled' })
}
if (!user) {
this.logFailedLoginAttempt(req, normalizedUsername, 'User not found')
return done(null, false, { message: 'User not found' })
}
// Update user's last seen
user.lastSeen = new Date()
await user.save()
this.logSuccessfulLoginAttempt(req, user.username)
return done(null, user)
} catch (error) {
Logger.error(`[ProxyAuthStrategy] Authentication error:`, error)
return done(error)
}
}
/**
* Log failed login attempt
* @param {import('express').Request} req
* @param {string} username
* @param {string} reason
*/
logFailedLoginAttempt(req, username, reason) {
const ip = req.ip || req.connection?.remoteAddress || 'Unknown'
Logger.warn(`[ProxyAuthStrategy] Failed login attempt for "${username}" from IP ${ip}: ${reason}`)
}
/**
* Log successful login attempt
* @param {import('express').Request} req
* @param {string} username
*/
logSuccessfulLoginAttempt(req, username) {
const ip = req.ip || req.connection?.remoteAddress || 'Unknown'
Logger.info(`[ProxyAuthStrategy] Successful proxy login for "${username}" from IP ${ip}`)
}
}
module.exports = ProxyAuthStrategy

View file

@ -64,6 +64,10 @@ class ServerSettings {
this.authLoginCustomMessage = null
this.authActiveAuthMethods = ['local']
// Proxy authentication settings
this.authProxyHeaderName = null
this.authProxyLogoutURL = null
// openid settings
this.authOpenIDIssuerURL = null
this.authOpenIDAuthorizationURL = null
@ -147,6 +151,9 @@ class ServerSettings {
this.authOpenIDAdvancedPermsClaim = settings.authOpenIDAdvancedPermsClaim || ''
this.authOpenIDSubfolderForRedirectURLs = settings.authOpenIDSubfolderForRedirectURLs
this.authProxyHeaderName = settings.authProxyHeaderName || null
this.authProxyLogoutURL = settings.authProxyLogoutURL || null
if (!Array.isArray(this.authActiveAuthMethods)) {
this.authActiveAuthMethods = ['local']
}
@ -200,6 +207,16 @@ class ServerSettings {
Logger.info(`[ServerSettings] Using allowIframe from environment variable`)
this.allowIframe = true
}
// Proxy authentication environment override
if (process.env.AUTH_PROXY_HEADER_NAME) {
Logger.info(`[ServerSettings] Using proxy header name from environment variable: ${process.env.AUTH_PROXY_HEADER_NAME}`)
this.authProxyHeaderName = process.env.AUTH_PROXY_HEADER_NAME
}
if (process.env.AUTH_PROXY_LOGOUT_URL) {
Logger.info(`[ServerSettings] Using proxy logout URL from environment variable: ${process.env.AUTH_PROXY_LOGOUT_URL}`)
this.authProxyLogoutURL = process.env.AUTH_PROXY_LOGOUT_URL
}
}
toJSON() {
@ -239,6 +256,8 @@ class ServerSettings {
buildNumber: this.buildNumber,
authLoginCustomMessage: this.authLoginCustomMessage,
authActiveAuthMethods: this.authActiveAuthMethods,
authProxyHeaderName: this.authProxyHeaderName,
authProxyLogoutURL: this.authProxyLogoutURL,
authOpenIDIssuerURL: this.authOpenIDIssuerURL,
authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL,
authOpenIDTokenURL: this.authOpenIDTokenURL,
@ -271,7 +290,7 @@ class ServerSettings {
}
get supportedAuthMethods() {
return ['local', 'openid']
return ['local', 'openid', 'proxy']
}
/**
@ -285,6 +304,8 @@ class ServerSettings {
return {
authLoginCustomMessage: this.authLoginCustomMessage,
authActiveAuthMethods: this.authActiveAuthMethods,
authProxyHeaderName: this.authProxyHeaderName,
authProxyLogoutURL: this.authProxyLogoutURL,
authOpenIDIssuerURL: this.authOpenIDIssuerURL,
authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL,
authOpenIDTokenURL: this.authOpenIDTokenURL,