mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-26 19:21:39 +00:00
Add support for forward proxy auth. Tested with authentik.
- adds new method (`getUserFromProxyAuth) to server/Auth.js to check for relevant environment variables and pull the user from the request headers.. - adds `/forwardauth` route to call new proxy auth method - modified /client/login.vue to call `/forwardauth` and authenticate the user if returned by forwardauth - add a helper method in `server/utils/parseBool.js` to help check for boolean values from environment - update the readme.md to explain how enable forward auth.
This commit is contained in:
parent
765a11f135
commit
b8d0395e6d
5 changed files with 111 additions and 0 deletions
|
|
@ -137,6 +137,34 @@ export default {
|
||||||
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
|
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
|
||||||
this.$store.commit('user/setUser', user)
|
this.$store.commit('user/setUser', user)
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Checks for forward proxy authentication
|
||||||
|
*/
|
||||||
|
async checkForwardAuth() {
|
||||||
|
this.processing = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
var authRes = await this.$axios.$post('/forwardauth').catch((error) => {
|
||||||
|
console.error('Failed', error.response)
|
||||||
|
if (error.response) this.error = error.response.data
|
||||||
|
else this.error = 'Unknown Error'
|
||||||
|
return false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authRes && authRes.disabled) {
|
||||||
|
// disabled == true - proxy auth has not been configured on the server, nothing further to do.
|
||||||
|
this.processing = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authRes && authRes.error) {
|
||||||
|
this.error = authRes.error
|
||||||
|
} else if (authRes) {
|
||||||
|
this.setUser(authRes)
|
||||||
|
}
|
||||||
|
this.processing = false
|
||||||
|
|
||||||
|
},
|
||||||
async submitForm() {
|
async submitForm() {
|
||||||
this.error = null
|
this.error = null
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
@ -202,6 +230,10 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
|
||||||
|
// Check for proxy auth
|
||||||
|
this.checkForwardAuth();
|
||||||
|
|
||||||
if (localStorage.getItem('token')) {
|
if (localStorage.getItem('token')) {
|
||||||
var userfound = await this.checkAuth()
|
var userfound = await this.checkAuth()
|
||||||
if (userfound) return // if valid user no need to check status
|
if (userfound) return // if valid user no need to check status
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,14 @@ Middleware relating to CORS will cause the app to report Unknown Error when logg
|
||||||
From [@Dondochaka](https://discord.com/channels/942908292873723984/942914154254176257/945074590374318170) and [@BeastleeUK](https://discord.com/channels/942908292873723984/942914154254176257/970366039294611506)
|
From [@Dondochaka](https://discord.com/channels/942908292873723984/942914154254176257/945074590374318170) and [@BeastleeUK](https://discord.com/channels/942908292873723984/942914154254176257/970366039294611506)
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
### Forward Proxy Authentication
|
||||||
|
Users can be authenticated via an external proxy such as Authentik.
|
||||||
|
To use forward authentication you need to set the following environment variables:
|
||||||
|
|
||||||
|
* `PROXY_FORWARD_AUTH_ENABLED` - enable/disable forward authentication
|
||||||
|
* `PROXY_FORWARD_AUTH_USERNAME` - the name of the header containing the authenticated user's username. For example, by default in Authentik the value would be `X-authentik-username`.
|
||||||
|
* `PROXY_FORWARD_AUTH_CREATE` - automatically create users if they don't exist.
|
||||||
|
|
||||||
# Run from source
|
# Run from source
|
||||||
|
|
||||||
[See discussion](https://github.com/advplyr/audiobookshelf/discussions/259#discussioncomment-1869729)
|
[See discussion](https://github.com/advplyr/audiobookshelf/discussions/259#discussioncomment-1869729)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
const bcrypt = require('./libs/bcryptjs')
|
const bcrypt = require('./libs/bcryptjs')
|
||||||
const jwt = require('./libs/jsonwebtoken')
|
const jwt = require('./libs/jsonwebtoken')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
const User = require('./objects/user/User')
|
||||||
|
const { getId } = require('./utils/index')
|
||||||
|
const { parseBool } = require('./utils/parseBool')
|
||||||
|
|
||||||
class Auth {
|
class Auth {
|
||||||
constructor(db) {
|
constructor(db) {
|
||||||
|
|
@ -170,6 +173,65 @@ 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
|
||||||
|
*/
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 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 = {
|
||||||
|
id: getId('usr'),
|
||||||
|
username: username,
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user)
|
||||||
|
return res.status(401).send(`User "${username}" not found`);
|
||||||
|
|
||||||
|
return res.json(this.getUserLoginResponsePayload(user, feeds))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
comparePassword(password, user) {
|
comparePassword(password, user) {
|
||||||
if (user.type === 'root' && !password && !user.pash) return true
|
if (user.type === 'root' && !password && !user.pash) return true
|
||||||
if (!password || !user.pash) return false
|
if (!password || !user.pash) return false
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@ class Server {
|
||||||
dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
|
dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
|
||||||
|
|
||||||
router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res, this.rssFeedManager.feedsArray))
|
router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res, this.rssFeedManager.feedsArray))
|
||||||
|
router.post('/forwardauth', (req, res) => this.auth.getUserFromProxyAuth(req, res, this.rssFeedManager.feedsArray))
|
||||||
router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
||||||
router.post('/init', (req, res) => {
|
router.post('/init', (req, res) => {
|
||||||
if (this.db.hasRootUser) {
|
if (this.db.hasRootUser) {
|
||||||
|
|
|
||||||
8
server/utils/parseBool.js
Normal file
8
server/utils/parseBool.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
const true_values = ["true", "yes", "y", "1", "on"];
|
||||||
|
|
||||||
|
module.exports.parseBool = (value) => {
|
||||||
|
if (Object.is(value, null) || value === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true_values.includes(String(value).toLowerCase());
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue