From f6de373388f2065e8c15c0d279fcaaae2bee1fce Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 24 Sep 2023 12:36:36 -0500 Subject: [PATCH] Update /status endpoint to return available auth methods, fix socket auth, update openid to use username instead of email --- client/pages/login.vue | 69 ++++++++++++++++++++------------------- server/Auth.js | 59 ++++++++++++++++----------------- server/Server.js | 3 +- server/SocketAuthority.js | 15 ++++++--- 4 files changed, 76 insertions(+), 70 deletions(-) diff --git a/client/pages/login.vue b/client/pages/login.vue index f9d82087..73cd2767 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -25,8 +25,11 @@

{{ $strings.HeaderLogin }}

+
+

{{ error }}

+
@@ -37,7 +40,9 @@ {{ processing ? 'Checking...' : $strings.ButtonSubmit }}
-
+ +
+
Login with Google @@ -106,6 +111,9 @@ export default { computed: { user() { return this.$store.state.user.user + }, + googleAuthUri() { + return `${process.env.serverUrl}/auth/openid?callback=${currentUrl}` } }, methods: { @@ -210,14 +218,16 @@ export default { this.processing = true this.$axios .$get('/status') - .then((res) => { + .then((data) => { this.processing = false - this.isInit = res.isInit - this.showInitScreen = !res.isInit - this.$setServerLanguageCode(res.language) + this.isInit = data.isInit + this.showInitScreen = !data.isInit + this.$setServerLanguageCode(data.language) if (this.showInitScreen) { - this.ConfigPath = res.ConfigPath || '' - this.MetadataPath = res.MetadataPath || '' + this.ConfigPath = data.ConfigPath || '' + this.MetadataPath = data.MetadataPath || '' + } else { + this.updateLoginVisibility(data.authMethods || []) } }) .catch((error) => { @@ -226,43 +236,34 @@ export default { this.criticalError = 'Status check failed' }) }, - async updateLoginVisibility() { - await this.$axios - .$get('/auth_methods') - .then((response) => { - if (response.includes('local')) { - this.login_local = true - } else { - this.login_local = false - } + updateLoginVisibility(authMethods) { + if (authMethods.includes('local') || !authMethods.length) { + this.login_local = true + } else { + this.login_local = false + } - if (response.includes('google-oauth20')) { - this.login_google_oauth20 = true - } else { - this.login_google_oauth20 = false - } + if (authMethods.includes('google-oauth20')) { + this.login_google_oauth20 = true + } else { + this.login_google_oauth20 = false + } - if (response.includes('openid')) { - this.login_openid = true - } else { - this.login_openid = false - } - }) - .catch((error) => { - console.error('Failed', error.response) - return false - }) + if (authMethods.includes('openid')) { + this.login_openid = true + } else { + this.login_openid = false + } } }, async mounted() { - this.$nextTick(async () => await this.updateLoginVisibility()) if (new URLSearchParams(window.location.search).get('setToken')) { localStorage.setItem('token', new URLSearchParams(window.location.search).get('setToken')) } if (localStorage.getItem('token')) { - var userfound = await this.checkAuth() - if (userfound) return // if valid user no need to check status + if (await this.checkAuth()) return // if valid user no need to check status } + this.checkStatus() } } diff --git a/server/Auth.js b/server/Auth.js index 13d7cde9..0041fbed 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -64,10 +64,9 @@ class Auth { (async function (issuer, profile, done) { // TODO: do we want to create the users which does not exist? - // get user by email - var user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase()) + const user = await Database.userModel.getUserByUsername(profile.username) - if (!user || !user.isActive) { + if (!user?.isActive) { // deny login done(null, null) return @@ -106,9 +105,10 @@ class Auth { } /** - * Stores the client's choise how the login callback should happen in temp cookies. - * @param {*} req Request object. - * @param {*} res Response object. + * Stores the client's choice how the login callback should happen in temp cookies + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ paramsToCookies(req, res) { if (req.query.isRest && req.query.isRest.toLowerCase() == "true") { @@ -140,12 +140,12 @@ class Auth { } } - /** * Informs the client in the right mode about a successfull login and the token * (clients choise is restored from cookies). - * @param {*} req Request object. - * @param {*} res Response object. + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async handleLoginSuccessBasedOnCookie(req, res) { // get userLogin json (information about the user, server and the session) @@ -170,16 +170,15 @@ class Auth { /** * Creates all (express) routes required for authentication. - * @param {express.Router} router + * + * @param {import('express').Router} router */ async initAuthRoutes(router) { // Local strategy login route (takes username and password) - router.post('/login', passport.authenticate('local'), - (async function (req, res) { - // return the user login response json if the login was successfull - res.json(await this.getUserLoginResponsePayload(req.user)) - }).bind(this) - ) + router.post('/login', passport.authenticate('local'), async (req, res) => { + // return the user login response json if the login was successfull + res.json(await this.getUserLoginResponsePayload(req.user)) + }) // google-oauth20 strategy login route (this redirects to the google login) router.get('/auth/google', (req, res, next) => { @@ -222,18 +221,13 @@ class Auth { } }) }) - - // Get avilible auth methods - router.get('/auth_methods', (req, res) => { - res.json(global.ServerSettings.authActiveAuthMethods) - }) } /** * middleware to use in express to only allow authenticated users. - * @param {express.Request} req - * @param {express.Response} res - * @param {express.NextFunction} next + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next */ isAuthenticated(req, res, next) { // check if session cookie says that we are authenticated @@ -246,18 +240,20 @@ class Auth { } /** - * Function to generate a jwt token for a given user. + * Function to generate a jwt token for a given user + * * @param {Object} user - * @returns the token. + * @returns {string} token */ generateAccessToken(user) { return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret) } /** - * Function to validate a jwt token for a given user. + * Function to validate a jwt token for a given user + * * @param {string} token - * @returns the tokens data. + * @returns {Object} tokens data */ static validateAccessToken(token) { try { @@ -365,9 +361,10 @@ class Auth { } /** - * Return the login info payload for a user. - * @param {string} username - * @returns {Promise} jsonPayload + * Return the login info payload for a user + * + * @param {Object} user + * @returns {Promise} jsonPayload */ async getUserLoginResponsePayload(user) { const libraryIds = await Database.libraryModel.getAllLibraryIds() diff --git a/server/Server.js b/server/Server.js index 2424456d..dbb9bddf 100644 --- a/server/Server.js +++ b/server/Server.js @@ -238,7 +238,8 @@ class Server { // server has been initialized if a root user exists const payload = { isInit: Database.hasRootUser, - language: Database.serverSettings.language + language: Database.serverSettings.language, + authMethods: Database.serverSettings.authActiveAuthMethods } if (!payload.isInit) { payload.ConfigPath = global.ConfigPath diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index 28e59e40..f1c69b24 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -146,24 +146,31 @@ class SocketAuthority { }) } - // When setting up a socket connection the user needs to be associated with a socket id - // for this the client will send a 'auth' event that includes the users API token + /** + * When setting up a socket connection the user needs to be associated with a socket id + * for this the client will send a 'auth' event that includes the users API token + * + * @param {SocketIO.Socket} socket + * @param {string} token JWT + */ async authenticateSocket(socket, token) { // we don't use passport to authenticate the jwt we get over the socket connection. // it's easier to directly verify/decode it. const token_data = Auth.validateAccessToken(token) - if (!token_data || !token_data.id) { + + if (!token_data?.userId) { // Token invalid Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') } // get the user via the id from the decoded jwt. - const user = await Database.userModel.getUserById(token_data.id) + const user = await Database.userModel.getUserByIdOrOldId(token_data.userId) if (!user) { // user not found Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') } + const client = this.clients[socket.id] if (!client) { Logger.error(`[SocketAuthority] Socket for user ${user.username} has no client`)