diff --git a/client/pages/login.vue b/client/pages/login.vue index a94045ae..8da68f5e 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -124,7 +124,7 @@ export default { location.reload() }, - setUser({ user, userDefaultLibraryId, serverSettings, Source, feeds }) { + setUser({ user, userDefaultLibraryId, serverSettings, Source }) { this.$store.commit('setServerSettings', serverSettings) this.$store.commit('setSource', Source) this.$setServerLanguageCode(serverSettings.language) @@ -143,17 +143,18 @@ export default { this.error = null this.processing = true - var payload = { + const payload = { username: this.username, password: this.password || '' } - var authRes = await this.$axios.$post('/login', payload).catch((error) => { + const authRes = await this.$axios.$post('/login', payload).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.error) { + console.log('Auth res', authRes) + if (authRes?.error) { this.error = authRes.error } else if (authRes) { this.setUser(authRes) diff --git a/package-lock.json b/package-lock.json index 0bfa07d2..e87a4b5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", - "passport-local": "^1.0.0", "passport-openidconnect": "^0.1.1", "socket.io": "^4.5.4", "xml2js": "^0.4.23" @@ -1109,17 +1108,6 @@ "passport-strategy": "^1.0.0" } }, - "node_modules/passport-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", - "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", - "dependencies": { - "passport-strategy": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/passport-oauth2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", @@ -2414,14 +2402,6 @@ "passport-strategy": "^1.0.0" } }, - "passport-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", - "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", - "requires": { - "passport-strategy": "1.x.x" - } - }, "passport-oauth2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", diff --git a/package.json b/package.json index b5ee0bd1..43ab49e2 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", - "passport-local": "^1.0.0", "passport-openidconnect": "^0.1.1", "socket.io": "^4.5.4", "xml2js": "^0.4.23" diff --git a/server/Auth.js b/server/Auth.js index 40bb64ac..9e1b2a12 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -1,12 +1,11 @@ const passport = require('passport') const bcrypt = require('./libs/bcryptjs') const jwt = require('./libs/jsonwebtoken') -const LocalStrategy = require('passport-local') -const JwtStrategy = require('passport-jwt').Strategy; -const ExtractJwt = require('passport-jwt').ExtractJwt; -const GoogleStrategy = require('passport-google-oauth20').Strategy; -var OpenIDConnectStrategy = require('passport-openidconnect'); -const User = require('./objects/user/User.js') +const LocalStrategy = require('./libs/passportLocal') +const JwtStrategy = require('passport-jwt').Strategy +const ExtractJwt = require('passport-jwt').ExtractJwt +const GoogleStrategy = require('passport-google-oauth20').Strategy +const OpenIDConnectStrategy = require('passport-openidconnect') /** * @class Class for handling all the authentication related functionality. @@ -35,7 +34,7 @@ class Auth { }, (function (accessToken, refreshToken, profile, done) { // TODO: what to use as username // TODO: do we want to create the users which does not exist? - var user = this.db.users.find(u => u.username.toLowerCase() === profile.emails[0].value.toLowerCase()) + const user = this.db.users.find(u => u.username.toLowerCase() === profile.emails[0].value.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -87,19 +86,19 @@ class Auth { return cb(null, JSON.stringify({ "username": user.username, "id": user.id, - })); - }); - }); + })) + }) + }) // define how to deseralize a user (use the username to get it from the database) passport.deserializeUser((function (user, cb) { process.nextTick((function () { const parsedUserInfo = JSON.parse(user) // TODO: do the matching on username or better on id? - var dbUser = this.db.users.find(u => u.username.toLowerCase() === parsedUserInfo.username.toLowerCase()) - return cb(null, new User(dbUser)); - }).bind(this)); - }).bind(this)); + const dbUser = this.db.users.find(u => u.username.toLowerCase() === parsedUserInfo.username.toLowerCase()) + return cb(null, dbUser) + }).bind(this)) + }).bind(this)) } /** @@ -107,19 +106,11 @@ class Auth { * @param {express.Router} router */ initAuthRoutes(router) { - // just a route saying "you need to login" where we redirect e.g. after logout - // TODO: replace with a 401? - router.get('/login', function (req, res) { - res.send('please login') - }) - // Local strategy login route (takes username and password) - router.post('/login', passport.authenticate('local', { - failureRedirect: '/login' - }), + router.post('/login', passport.authenticate('local'), (function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user.username)) + res.json(this.getUserLoginResponsePayload(req.user)) }).bind(this) ) @@ -128,30 +119,35 @@ class Auth { // google-oauth20 strategy callback route (this receives the token from google) router.get('/auth/google/callback', - passport.authenticate('google', { failureRedirect: '/login' }), + passport.authenticate('google'), (function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user.username)) + res.json(this.getUserLoginResponsePayload(req.user)) }).bind(this) ) // openid strategy login route (this redirects to the configured openid login provider) - router.get('/auth/openid', passport.authenticate('openidconnect')); + router.get('/auth/openid', passport.authenticate('openidconnect')) // openid strategy callback route (this receives the token from the configured openid login provider) router.get('/auth/openid/callback', - passport.authenticate('openidconnect', { failureRedirect: '/login' }), + passport.authenticate('openidconnect'), (function (req, res) { // return the user login response json if the login was successfull - res.json(this.getUserLoginResponsePayload(req.user.username)) + res.json(this.getUserLoginResponsePayload(req.user)) }).bind(this) ) // Logout route - router.get('/logout', function (req, res) { + router.post('/logout', (req, res) => { // TODO: invalidate possible JWTs - req.logout() - res.redirect('/login') + req.logout((err) => { + if (err) { + res.sendStatus(500) + } else { + res.sendStatus(200) + } + }) }) } @@ -177,7 +173,7 @@ class Auth { * @returns the token. */ generateAccessToken(user) { - return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret); + return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret) } /** @@ -206,7 +202,7 @@ class Auth { * @param {function} done */ jwtAuthCheck(jwt_payload, done) { - var user = this.db.users.find(u => u.username.toLowerCase() === jwt_payload.username.toLowerCase()) + const user = this.db.users.find(u => u.username.toLowerCase() === jwt_payload.username.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -217,13 +213,13 @@ class Auth { } /** - * Checks if a username and passpword touple is valid and the user active. + * Checks if a username and password tuple is valid and the user active. * @param {string} username * @param {string} password * @param {function} done */ - localAuthCheckUserPw(username, password, done) { - var user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) + async localAuthCheckUserPw(username, password, done) { + const user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) if (!user || !user.isActive) { done(null, null) @@ -241,7 +237,7 @@ class Auth { } // Check password match - var compare = bcrypt.compareSync(password, user.pash) + const compare = await bcrypt.compare(password, user.pash) if (compare) { done(null, user) return @@ -272,9 +268,7 @@ class Auth { * @param {string} username * @returns {string} jsonPayload */ - getUserLoginResponsePayload(username) { - var user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) - user = new User(user) + getUserLoginResponsePayload(user) { return { user: user.toJSONForBrowser(), userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries), diff --git a/server/Server.js b/server/Server.js index d2865b88..96896a73 100644 --- a/server/Server.js +++ b/server/Server.js @@ -161,7 +161,7 @@ class Server { // config passport.js this.auth.initPassportJs() // use auth on all routes - not used now - // app.use(passport.authenticate('session')); + // app.use(passport.authenticate('session')) const router = express.Router() app.use(global.RouterBasePath, router) @@ -169,9 +169,8 @@ class Server { this.server = http.createServer(app) - // router.use(this.auth.cors) router.use(fileUpload()) - router.use(express.urlencoded({ extended: true, limit: "5mb" })); + router.use(express.urlencoded({ extended: true, limit: "5mb" })) router.use(express.json({ limit: "5mb" })) // Static path to generated nuxt diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index bf6ea6f7..a12bca29 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -140,7 +140,7 @@ 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 async authenticateSocket(socket, token) { - const user = await this.Server.auth.authenticateUser(token) + const user = await this.Server.db.users.find(u => u.token === token) if (!user) { Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') diff --git a/server/libs/passportLocal/LICENSE b/server/libs/passportLocal/LICENSE new file mode 100644 index 00000000..d8ebfcf1 --- /dev/null +++ b/server/libs/passportLocal/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2011-2014 Jared Hanson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/server/libs/passportLocal/index.js b/server/libs/passportLocal/index.js new file mode 100644 index 00000000..365d4f65 --- /dev/null +++ b/server/libs/passportLocal/index.js @@ -0,0 +1,20 @@ +// +// modified for audiobookshelf +// Source: https://github.com/jaredhanson/passport-local +// + +/** + * Module dependencies. + */ +var Strategy = require('./strategy'); + + +/** + * Expose `Strategy` directly from package. + */ +exports = module.exports = Strategy; + +/** + * Export constructors. + */ +exports.Strategy = Strategy; diff --git a/server/libs/passportLocal/strategy.js b/server/libs/passportLocal/strategy.js new file mode 100644 index 00000000..67110204 --- /dev/null +++ b/server/libs/passportLocal/strategy.js @@ -0,0 +1,119 @@ +/** + * Module dependencies. + */ +const passport = require('passport-strategy') +const util = require('util') + + +function lookup(obj, field) { + if (!obj) { return null; } + var chain = field.split(']').join('').split('['); + for (var i = 0, len = chain.length; i < len; i++) { + var prop = obj[chain[i]]; + if (typeof (prop) === 'undefined') { return null; } + if (typeof (prop) !== 'object') { return prop; } + obj = prop; + } + return null; +} + +/** + * `Strategy` constructor. + * + * The local authentication strategy authenticates requests based on the + * credentials submitted through an HTML-based login form. + * + * Applications must supply a `verify` callback which accepts `username` and + * `password` credentials, and then calls the `done` callback supplying a + * `user`, which should be set to `false` if the credentials are not valid. + * If an exception occured, `err` should be set. + * + * Optionally, `options` can be used to change the fields in which the + * credentials are found. + * + * Options: + * - `usernameField` field name where the username is found, defaults to _username_ + * - `passwordField` field name where the password is found, defaults to _password_ + * - `passReqToCallback` when `true`, `req` is the first argument to the verify callback (default: `false`) + * + * Examples: + * + * passport.use(new LocalStrategy( + * function(username, password, done) { + * User.findOne({ username: username, password: password }, function (err, user) { + * done(err, user); + * }); + * } + * )); + * + * @param {Object} options + * @param {Function} verify + * @api public + */ +function Strategy(options, verify) { + if (typeof options == 'function') { + verify = options; + options = {}; + } + if (!verify) { throw new TypeError('LocalStrategy requires a verify callback'); } + + this._usernameField = options.usernameField || 'username'; + this._passwordField = options.passwordField || 'password'; + + passport.Strategy.call(this); + this.name = 'local'; + this._verify = verify; + this._passReqToCallback = options.passReqToCallback; +} + +/** + * Inherit from `passport.Strategy`. + */ +util.inherits(Strategy, passport.Strategy); + +/** + * Authenticate request based on the contents of a form submission. + * + * @param {Object} req + * @api protected + */ +Strategy.prototype.authenticate = function (req, options) { + options = options || {}; + var username = lookup(req.body, this._usernameField) + if (username === null) { + lookup(req.query, this._usernameField); + } + + var password = lookup(req.body, this._passwordField) + if (password === null) { + password = lookup(req.query, this._passwordField); + } + + if (username === null || password === null) { + return this.fail({ message: options.badRequestMessage || 'Missing credentials' }, 400); + } + + var self = this; + + function verified(err, user, info) { + if (err) { return self.error(err); } + if (!user) { return self.fail(info); } + self.success(user, info); + } + + try { + if (self._passReqToCallback) { + this._verify(req, username, password, verified); + } else { + this._verify(username, password, verified); + } + } catch (ex) { + return self.error(ex); + } +}; + + +/** + * Expose `Strategy`. + */ +module.exports = Strategy; diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 602d8ac1..2a51e3de 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -1,5 +1,4 @@ const { BookshelfView } = require('../../utils/constants') -const { isNullOrNaN } = require('../../utils') const Logger = require('../../Logger') class ServerSettings { @@ -144,7 +143,7 @@ class ServerSettings { this.authGoogleOauth20ClientSecret === '' || this.authGoogleOauth20CallbackURL === '' )) { - this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1); + this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('google-oauth20', 0), 1) } // remove uninitialized methods @@ -158,7 +157,7 @@ class ServerSettings { this.authOpenIDClientSecret === '' || this.authOpenIDCallbackURL === '' )) { - this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('generic-oauth20', 0), 1); + this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('generic-oauth20', 0), 1) } // fallback to local @@ -241,10 +240,10 @@ class ServerSettings { } update(payload) { - var hasUpdates = false + let hasUpdates = false for (const key in payload) { if (key === 'sortingPrefixes' && payload[key] && payload[key].length) { - var prefixesCleaned = payload[key].filter(prefix => !!prefix).map(prefix => prefix.toLowerCase()) + const prefixesCleaned = payload[key].filter(prefix => !!prefix).map(prefix => prefix.toLowerCase()) if (prefixesCleaned.join(',') !== this[key].join(',')) { this[key] = [...prefixesCleaned] hasUpdates = true