audiobookshelf/server/Auth.js

287 lines
9.4 KiB
JavaScript

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')
/**
* @class Class for handling all the authentication related functionality.
*/
class Auth {
constructor(db) {
this.db = db
}
/**
* Inializes all passportjs stragegies and other passportjs ralated initialization.
*/
initPassportJs() {
// Check if we should load the local strategy
if (global.ServerSettings.authActiveAuthMethods.includes("local")) {
passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this)))
}
// Check if we should load the google-oauth20 strategy
if (global.ServerSettings.authActiveAuthMethods.includes("google-oauth20")) {
passport.use(new GoogleStrategy({
clientID: global.ServerSettings.authGoogleOauth20ClientID,
clientSecret: global.ServerSettings.authGoogleOauth20ClientSecret,
callbackURL: global.ServerSettings.authGoogleOauth20CallbackURL
}, (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())
if (!user || !user.isActive) {
done(null, null)
return
}
return done(null, user)
}).bind(this)))
}
// Check if we should load the openid strategy
if (global.ServerSettings.authActiveAuthMethods.includes("openid")) {
passport.use(new OpenIDConnectStrategy({
issuer: global.ServerSettings.authOpenIDIssuerURL,
authorizationURL: global.ServerSettings.authOpenIDAuthorizationURL,
tokenURL: global.ServerSettings.authOpenIDTokenURL,
userInfoURL: global.ServerSettings.authOpenIDUserInfoURL,
clientID: global.ServerSettings.authOpenIDClientID,
clientSecret: global.ServerSettings.authOpenIDClientSecret,
callbackURL: global.ServerSettings.authOpenIDCallbackURL,
scope: ["openid", "email", "profile"],
skipUserProfile: false
},
(function (issuer, 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())
if (!user || !user.isActive) {
done(null, null)
return
}
return done(null, user)
}).bind(this)))
}
// Load the JwtStrategy (always) -> for bearer token auth
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: global.ServerSettings.tokenSecret
}, this.jwtAuthCheck.bind(this)))
// define how to seralize a user (to be put into the session)
passport.serializeUser(function (user, cb) {
process.nextTick(function () {
// only store username and id to session
// TODO: do we want to store more info in the session?
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));
}
/**
* Creates all (express) routes required for authentication.
* @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'
}),
(function (req, res) {
// return the user login response json if the login was successfull
res.json(this.getUserLoginResponsePayload(req.user.username))
}).bind(this)
)
// google-oauth20 strategy login route (this redirects to the google login)
router.get('/auth/google', passport.authenticate('google', { scope: ['email'] }))
// google-oauth20 strategy callback route (this receives the token from google)
router.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(function (req, res) {
// return the user login response json if the login was successfull
res.json(this.getUserLoginResponsePayload(req.user.username))
}).bind(this)
)
// openid strategy login route (this redirects to the configured openid login provider)
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' }),
(function (req, res) {
// return the user login response json if the login was successfull
res.json(this.getUserLoginResponsePayload(req.user.username))
}).bind(this)
)
// Logout route
router.get('/logout', function (req, res) {
// TODO: invalidate possible JWTs
req.logout()
res.redirect('/login')
})
}
/**
* middleware to use in express to only allow authenticated users.
* @param {express.Request} req
* @param {express.Response} res
* @param {express.NextFunction} next
*/
isAuthenticated(req, res, next) {
// check if session cookie says that we are authenticated
if (req.isAuthenticated()) {
next()
} else {
// try JWT to authenticate
passport.authenticate("jwt")(req, res, next)
}
}
/**
* Function to generate a jwt token for a given user.
* @param {Object} user
* @returns the token.
*/
generateAccessToken(user) {
return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret);
}
/**
* Generate a token for each user.
*/
async initTokenSecret() {
if (process.env.TOKEN_SECRET) { // User can supply their own token secret
this.db.serverSettings.tokenSecret = process.env.TOKEN_SECRET
} else {
this.db.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64')
}
await this.db.updateServerSettings()
// New token secret creation added in v2.1.0 so generate new API tokens for each user
if (this.db.users.length) {
for (const user of this.db.users) {
user.token = await this.generateAccessToken({ userId: user.id, username: user.username })
}
await this.db.updateEntities('user', this.db.users)
}
}
/**
* Checks if the user in the validated jwt_payload really exists and is active.
* @param {Object} jwt_payload
* @param {function} done
*/
jwtAuthCheck(jwt_payload, done) {
var user = this.db.users.find(u => u.username.toLowerCase() === jwt_payload.username.toLowerCase())
if (!user || !user.isActive) {
done(null, null)
return
}
done(null, user)
return
}
/**
* Checks if a username and passpword touple 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())
if (!user || !user.isActive) {
done(null, null)
return
}
// Check passwordless root user
if (user.id === 'root' && (!user.pash || user.pash === '')) {
if (password) {
done(null, null)
return
}
done(null, user)
return
}
// Check password match
var compare = bcrypt.compareSync(password, user.pash)
if (compare) {
done(null, user)
return
}
done(null, null)
return
}
/**
* Hashes a password with bcrypt.
* @param {string} password
* @returns {string} hash
*/
hashPass(password) {
return new Promise((resolve) => {
bcrypt.hash(password, 8, (err, hash) => {
if (err) {
resolve(null)
} else {
resolve(hash)
}
})
})
}
/**
* Return the login info payload for a user.
* @param {string} username
* @returns {string} jsonPayload
*/
getUserLoginResponsePayload(username) {
var user = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase())
user = new User(user)
return {
user: user.toJSONForBrowser(),
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
serverSettings: this.db.serverSettings.toJSONForBrowser(),
Source: global.Source
}
}
}
module.exports = Auth