From b8d0395e6d58a39b2ece6d3541ac2a8d5fbaea53 Mon Sep 17 00:00:00 2001 From: pgodwin Date: Wed, 2 Nov 2022 22:03:06 +1000 Subject: [PATCH] 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. --- client/pages/login.vue | 32 ++++++++++++++++++++ readme.md | 8 +++++ server/Auth.js | 62 +++++++++++++++++++++++++++++++++++++++ server/Server.js | 1 + server/utils/parseBool.js | 8 +++++ 5 files changed, 111 insertions(+) create mode 100644 server/utils/parseBool.js diff --git a/client/pages/login.vue b/client/pages/login.vue index 50a6fd56c..9858d17d6 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -137,6 +137,34 @@ export default { this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId) 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() { this.error = null this.processing = true @@ -202,6 +230,10 @@ export default { } }, async mounted() { + + // Check for proxy auth + this.checkForwardAuth(); + if (localStorage.getItem('token')) { var userfound = await this.checkAuth() if (userfound) return // if valid user no need to check status diff --git a/readme.md b/readme.md index ba389a819..537361a7f 100644 --- a/readme.md +++ b/readme.md @@ -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)
+### 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 [See discussion](https://github.com/advplyr/audiobookshelf/discussions/259#discussioncomment-1869729) diff --git a/server/Auth.js b/server/Auth.js index a8fd043e2..acdf66f45 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -1,6 +1,9 @@ const bcrypt = require('./libs/bcryptjs') const jwt = require('./libs/jsonwebtoken') const Logger = require('./Logger') +const User = require('./objects/user/User') +const { getId } = require('./utils/index') +const { parseBool } = require('./utils/parseBool') class Auth { 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) { if (user.type === 'root' && !password && !user.pash) return true if (!password || !user.pash) return false diff --git a/server/Server.js b/server/Server.js index b03feb6d6..4d4e1a56a 100644 --- a/server/Server.js +++ b/server/Server.js @@ -242,6 +242,7 @@ class Server { 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('/forwardauth', (req, res) => this.auth.getUserFromProxyAuth(req, res, this.rssFeedManager.feedsArray)) router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this)) router.post('/init', (req, res) => { if (this.db.hasRootUser) { diff --git a/server/utils/parseBool.js b/server/utils/parseBool.js new file mode 100644 index 000000000..546c5e6a1 --- /dev/null +++ b/server/utils/parseBool.js @@ -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()); +} \ No newline at end of file