mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-11-24 20:05:41 +01:00
feat: proxy authenfication added
This commit is contained in:
parent
03da194953
commit
4875125ae9
@ -122,6 +122,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ui-checkbox v-model="enableProxyAuth" checkbox-bg="bg" />
|
||||||
|
<p class="text-lg pl-4">{{ $strings.HeaderProxyAuthentication }}</p>
|
||||||
|
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||||
|
<a href="https://www.audiobookshelf.org/guides/reverse_proxy_authentication" target="_blank" class="inline-flex">
|
||||||
|
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||||
|
</a>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<transition name="slide">
|
||||||
|
<div v-if="enableProxyAuth" class="flex flex-wrap pt-4">
|
||||||
|
<ui-text-input-with-label ref="proxyHeaderName" v-model="newAuthSettings.authProxyHeaderName" :disabled="savingSettings" :label="$strings.LabelProxyHeaderName" :placeholder="'X-Remote-User'" class="mb-2" />
|
||||||
|
<p class="text-sm text-gray-300 mb-4">
|
||||||
|
{{ $strings.LabelProxyHeaderNameDescription }}
|
||||||
|
</p>
|
||||||
|
<ui-text-input-with-label ref="proxyLogoutURL" v-model="newAuthSettings.authProxyLogoutURL" :disabled="savingSettings" :label="$strings.LabelProxyLogoutUrl" :placeholder="'https://proxy.example.com/logout'" class="mb-2" />
|
||||||
|
<p class="text-sm text-gray-300 mb-4">
|
||||||
|
{{ $strings.LabelProxyLogoutUrlDescription }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex items-center justify-between p-4">
|
<div class="w-full flex items-center justify-between p-4">
|
||||||
<p v-if="enableOpenIDAuth" class="text-sm text-warning">{{ $strings.MessageAuthenticationOIDCChangesRestart }}</p>
|
<p v-if="enableOpenIDAuth" class="text-sm text-warning">{{ $strings.MessageAuthenticationOIDCChangesRestart }}</p>
|
||||||
<ui-btn color="bg-success" :padding-x="8" small class="text-base" :loading="savingSettings" @click="saveSettings">{{ $strings.ButtonSave }}</ui-btn>
|
<ui-btn color="bg-success" :padding-x="8" small class="text-base" :loading="savingSettings" @click="saveSettings">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
@ -154,6 +180,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
enableLocalAuth: false,
|
enableLocalAuth: false,
|
||||||
enableOpenIDAuth: false,
|
enableOpenIDAuth: false,
|
||||||
|
enableProxyAuth: false,
|
||||||
showCustomLoginMessage: false,
|
showCustomLoginMessage: false,
|
||||||
savingSettings: false,
|
savingSettings: false,
|
||||||
openIdSigningAlgorithmsSupportedByIssuer: [],
|
openIdSigningAlgorithmsSupportedByIssuer: [],
|
||||||
@ -323,7 +350,7 @@ export default {
|
|||||||
return isValid
|
return isValid
|
||||||
},
|
},
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
if (!this.enableLocalAuth && !this.enableOpenIDAuth) {
|
if (!this.enableLocalAuth && !this.enableOpenIDAuth && !this.enableProxyAuth) {
|
||||||
this.$toast.error('Must have at least one authentication method enabled')
|
this.$toast.error('Must have at least one authentication method enabled')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -332,6 +359,11 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.enableProxyAuth && !this.newAuthSettings.authProxyHeaderName?.trim()) {
|
||||||
|
this.$toast.error('Authentication Header Name is required for proxy authentication')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.showCustomLoginMessage || !this.newAuthSettings.authLoginCustomMessage?.trim()) {
|
if (!this.showCustomLoginMessage || !this.newAuthSettings.authLoginCustomMessage?.trim()) {
|
||||||
this.newAuthSettings.authLoginCustomMessage = null
|
this.newAuthSettings.authLoginCustomMessage = null
|
||||||
}
|
}
|
||||||
@ -339,6 +371,7 @@ export default {
|
|||||||
this.newAuthSettings.authActiveAuthMethods = []
|
this.newAuthSettings.authActiveAuthMethods = []
|
||||||
if (this.enableLocalAuth) this.newAuthSettings.authActiveAuthMethods.push('local')
|
if (this.enableLocalAuth) this.newAuthSettings.authActiveAuthMethods.push('local')
|
||||||
if (this.enableOpenIDAuth) this.newAuthSettings.authActiveAuthMethods.push('openid')
|
if (this.enableOpenIDAuth) this.newAuthSettings.authActiveAuthMethods.push('openid')
|
||||||
|
if (this.enableProxyAuth) this.newAuthSettings.authActiveAuthMethods.push('proxy')
|
||||||
|
|
||||||
this.savingSettings = true
|
this.savingSettings = true
|
||||||
this.$axios
|
this.$axios
|
||||||
@ -366,6 +399,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.enableLocalAuth = this.authMethods.includes('local')
|
this.enableLocalAuth = this.authMethods.includes('local')
|
||||||
this.enableOpenIDAuth = this.authMethods.includes('openid')
|
this.enableOpenIDAuth = this.authMethods.includes('openid')
|
||||||
|
this.enableProxyAuth = this.authMethods.includes('proxy')
|
||||||
this.showCustomLoginMessage = !!this.authSettings.authLoginCustomMessage
|
this.showCustomLoginMessage = !!this.authSettings.authLoginCustomMessage
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -222,6 +222,32 @@ export default {
|
|||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
},
|
},
|
||||||
|
async attemptProxyAuth() {
|
||||||
|
this.error = null
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authRes = await this.$axios.$post('/auth/proxy').catch((error) => {
|
||||||
|
console.error('Proxy auth failed', error.response)
|
||||||
|
if (error.response?.data?.message) {
|
||||||
|
this.error = error.response.data.message
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (authRes?.error) {
|
||||||
|
this.error = authRes.error
|
||||||
|
} else if (authRes) {
|
||||||
|
this.setUser(authRes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Proxy auth error', error)
|
||||||
|
this.error = 'Proxy authentication failed'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = false
|
||||||
|
},
|
||||||
checkAuth() {
|
checkAuth() {
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
if (!token) return false
|
if (!token) return false
|
||||||
@ -307,6 +333,11 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.login_openid = false
|
this.login_openid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authMethods.includes('proxy')) {
|
||||||
|
// Auto-attempt proxy authentication
|
||||||
|
this.attemptProxyAuth()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
|||||||
@ -175,6 +175,7 @@
|
|||||||
"HeaderOpenListeningSessions": "Open Listening Sessions",
|
"HeaderOpenListeningSessions": "Open Listening Sessions",
|
||||||
"HeaderOpenRSSFeed": "Open RSS Feed",
|
"HeaderOpenRSSFeed": "Open RSS Feed",
|
||||||
"HeaderOtherFiles": "Other Files",
|
"HeaderOtherFiles": "Other Files",
|
||||||
|
"HeaderProxyAuthentication": "Proxy Authentication",
|
||||||
"HeaderPasswordAuthentication": "Password Authentication",
|
"HeaderPasswordAuthentication": "Password Authentication",
|
||||||
"HeaderPermissions": "Permissions",
|
"HeaderPermissions": "Permissions",
|
||||||
"HeaderPlayerQueue": "Player Queue",
|
"HeaderPlayerQueue": "Player Queue",
|
||||||
@ -531,6 +532,10 @@
|
|||||||
"LabelProgress": "Progress",
|
"LabelProgress": "Progress",
|
||||||
"LabelProvider": "Provider",
|
"LabelProvider": "Provider",
|
||||||
"LabelProviderAuthorizationValue": "Authorization Header Value",
|
"LabelProviderAuthorizationValue": "Authorization Header Value",
|
||||||
|
"LabelProxyHeaderName": "Authentication Header Name",
|
||||||
|
"LabelProxyHeaderNameDescription": "The name of the header that your proxy uses to pass the authenticated username to Audiobookshelf.",
|
||||||
|
"LabelProxyLogoutUrl": "Custom Logout URL",
|
||||||
|
"LabelProxyLogoutUrlDescription": "The URL users will be redirected to when they click the logout button. This should log the user out of your authenfication proxy.",
|
||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
"LabelPublishedDate": "Published {0}",
|
"LabelPublishedDate": "Published {0}",
|
||||||
|
|||||||
@ -8,6 +8,7 @@ const Logger = require('./Logger')
|
|||||||
const TokenManager = require('./auth/TokenManager')
|
const TokenManager = require('./auth/TokenManager')
|
||||||
const LocalAuthStrategy = require('./auth/LocalAuthStrategy')
|
const LocalAuthStrategy = require('./auth/LocalAuthStrategy')
|
||||||
const OidcAuthStrategy = require('./auth/OidcAuthStrategy')
|
const OidcAuthStrategy = require('./auth/OidcAuthStrategy')
|
||||||
|
const ProxyAuthStrategy = require('./auth/ProxyAuthStrategy')
|
||||||
|
|
||||||
const RateLimiterFactory = require('./utils/rateLimiterFactory')
|
const RateLimiterFactory = require('./utils/rateLimiterFactory')
|
||||||
const { escapeRegExp } = require('./utils')
|
const { escapeRegExp } = require('./utils')
|
||||||
@ -26,6 +27,7 @@ class Auth {
|
|||||||
this.tokenManager = new TokenManager()
|
this.tokenManager = new TokenManager()
|
||||||
this.localAuthStrategy = new LocalAuthStrategy()
|
this.localAuthStrategy = new LocalAuthStrategy()
|
||||||
this.oidcAuthStrategy = new OidcAuthStrategy()
|
this.oidcAuthStrategy = new OidcAuthStrategy()
|
||||||
|
this.proxyAuthStrategy = new ProxyAuthStrategy()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,6 +61,31 @@ class Auth {
|
|||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
isAuthenticated(req, res, 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)
|
return passport.authenticate('jwt', { session: false })(req, res, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,6 +146,11 @@ class Auth {
|
|||||||
this.oidcAuthStrategy.init()
|
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
|
// Load the JwtStrategy (always) -> for bearer token auth
|
||||||
passport.use(
|
passport.use(
|
||||||
new JwtStrategy(
|
new JwtStrategy(
|
||||||
@ -171,6 +203,8 @@ class Auth {
|
|||||||
this.oidcAuthStrategy.unuse()
|
this.oidcAuthStrategy.unuse()
|
||||||
} else if (name === 'local') {
|
} else if (name === 'local') {
|
||||||
this.localAuthStrategy.unuse()
|
this.localAuthStrategy.unuse()
|
||||||
|
} else if (name === 'proxy') {
|
||||||
|
this.proxyAuthStrategy.unuse()
|
||||||
} else {
|
} else {
|
||||||
Logger.error('[Auth] Invalid auth strategy ' + name)
|
Logger.error('[Auth] Invalid auth strategy ' + name)
|
||||||
}
|
}
|
||||||
@ -186,6 +220,8 @@ class Auth {
|
|||||||
this.oidcAuthStrategy.init()
|
this.oidcAuthStrategy.init()
|
||||||
} else if (name === 'local') {
|
} else if (name === 'local') {
|
||||||
this.localAuthStrategy.init()
|
this.localAuthStrategy.init()
|
||||||
|
} else if (name === 'proxy') {
|
||||||
|
this.proxyAuthStrategy.init()
|
||||||
} else {
|
} else {
|
||||||
Logger.error('[Auth] Invalid auth strategy ' + name)
|
Logger.error('[Auth] Invalid auth strategy ' + name)
|
||||||
}
|
}
|
||||||
@ -325,6 +361,18 @@ class Auth {
|
|||||||
res.json(userResponse)
|
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
|
// Refresh token route
|
||||||
router.post('/auth/refresh', this.authRateLimiter, async (req, res) => {
|
router.post('/auth/refresh', this.authRateLimiter, async (req, res) => {
|
||||||
let refreshToken = req.cookies.refresh_token
|
let refreshToken = req.cookies.refresh_token
|
||||||
@ -501,6 +549,12 @@ class Auth {
|
|||||||
if (authMethod === 'openid' || authMethod === 'openid-mobile') {
|
if (authMethod === 'openid' || authMethod === 'openid-mobile') {
|
||||||
logoutUrl = this.oidcAuthStrategy.getEndSessionUrl(req, req.cookies.openid_id_token, authMethod)
|
logoutUrl = this.oidcAuthStrategy.getEndSessionUrl(req, req.cookies.openid_id_token, authMethod)
|
||||||
res.clearCookie('openid_id_token')
|
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
|
// Tell the user agent (browser) to redirect to the authentification provider's logout URL
|
||||||
|
|||||||
168
server/auth/ProxyAuthStrategy.js
Normal file
168
server/auth/ProxyAuthStrategy.js
Normal 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
|
||||||
@ -64,6 +64,10 @@ class ServerSettings {
|
|||||||
this.authLoginCustomMessage = null
|
this.authLoginCustomMessage = null
|
||||||
this.authActiveAuthMethods = ['local']
|
this.authActiveAuthMethods = ['local']
|
||||||
|
|
||||||
|
// Proxy authentication settings
|
||||||
|
this.authProxyHeaderName = null
|
||||||
|
this.authProxyLogoutURL = null
|
||||||
|
|
||||||
// openid settings
|
// openid settings
|
||||||
this.authOpenIDIssuerURL = null
|
this.authOpenIDIssuerURL = null
|
||||||
this.authOpenIDAuthorizationURL = null
|
this.authOpenIDAuthorizationURL = null
|
||||||
@ -147,6 +151,9 @@ class ServerSettings {
|
|||||||
this.authOpenIDAdvancedPermsClaim = settings.authOpenIDAdvancedPermsClaim || ''
|
this.authOpenIDAdvancedPermsClaim = settings.authOpenIDAdvancedPermsClaim || ''
|
||||||
this.authOpenIDSubfolderForRedirectURLs = settings.authOpenIDSubfolderForRedirectURLs
|
this.authOpenIDSubfolderForRedirectURLs = settings.authOpenIDSubfolderForRedirectURLs
|
||||||
|
|
||||||
|
this.authProxyHeaderName = settings.authProxyHeaderName || null
|
||||||
|
this.authProxyLogoutURL = settings.authProxyLogoutURL || null
|
||||||
|
|
||||||
if (!Array.isArray(this.authActiveAuthMethods)) {
|
if (!Array.isArray(this.authActiveAuthMethods)) {
|
||||||
this.authActiveAuthMethods = ['local']
|
this.authActiveAuthMethods = ['local']
|
||||||
}
|
}
|
||||||
@ -200,6 +207,16 @@ class ServerSettings {
|
|||||||
Logger.info(`[ServerSettings] Using allowIframe from environment variable`)
|
Logger.info(`[ServerSettings] Using allowIframe from environment variable`)
|
||||||
this.allowIframe = true
|
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() {
|
toJSON() {
|
||||||
@ -239,6 +256,8 @@ class ServerSettings {
|
|||||||
buildNumber: this.buildNumber,
|
buildNumber: this.buildNumber,
|
||||||
authLoginCustomMessage: this.authLoginCustomMessage,
|
authLoginCustomMessage: this.authLoginCustomMessage,
|
||||||
authActiveAuthMethods: this.authActiveAuthMethods,
|
authActiveAuthMethods: this.authActiveAuthMethods,
|
||||||
|
authProxyHeaderName: this.authProxyHeaderName,
|
||||||
|
authProxyLogoutURL: this.authProxyLogoutURL,
|
||||||
authOpenIDIssuerURL: this.authOpenIDIssuerURL,
|
authOpenIDIssuerURL: this.authOpenIDIssuerURL,
|
||||||
authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL,
|
authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL,
|
||||||
authOpenIDTokenURL: this.authOpenIDTokenURL,
|
authOpenIDTokenURL: this.authOpenIDTokenURL,
|
||||||
@ -271,7 +290,7 @@ class ServerSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get supportedAuthMethods() {
|
get supportedAuthMethods() {
|
||||||
return ['local', 'openid']
|
return ['local', 'openid', 'proxy']
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -285,6 +304,8 @@ class ServerSettings {
|
|||||||
return {
|
return {
|
||||||
authLoginCustomMessage: this.authLoginCustomMessage,
|
authLoginCustomMessage: this.authLoginCustomMessage,
|
||||||
authActiveAuthMethods: this.authActiveAuthMethods,
|
authActiveAuthMethods: this.authActiveAuthMethods,
|
||||||
|
authProxyHeaderName: this.authProxyHeaderName,
|
||||||
|
authProxyLogoutURL: this.authProxyLogoutURL,
|
||||||
authOpenIDIssuerURL: this.authOpenIDIssuerURL,
|
authOpenIDIssuerURL: this.authOpenIDIssuerURL,
|
||||||
authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL,
|
authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL,
|
||||||
authOpenIDTokenURL: this.authOpenIDTokenURL,
|
authOpenIDTokenURL: this.authOpenIDTokenURL,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user