2023-03-24 18:21:25 +01:00
|
|
|
const passport = require('passport')
|
2022-07-07 02:01:27 +02:00
|
|
|
const bcrypt = require('./libs/bcryptjs')
|
2022-07-07 01:45:43 +02:00
|
|
|
const jwt = require('./libs/jsonwebtoken')
|
2023-04-16 17:08:13 +02:00
|
|
|
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')
|
2023-09-13 18:35:39 +02:00
|
|
|
const Database = require('./Database')
|
2023-03-24 18:21:25 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @class Class for handling all the authentication related functionality.
|
|
|
|
*/
|
2021-08-18 00:01:11 +02:00
|
|
|
class Auth {
|
2023-03-24 18:21:25 +01:00
|
|
|
|
2023-09-13 18:35:39 +02:00
|
|
|
constructor() {
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
|
2023-03-24 18:21:25 +01:00
|
|
|
/**
|
|
|
|
* 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)))
|
|
|
|
}
|
2023-04-14 20:26:29 +02:00
|
|
|
|
2023-03-24 18:21:25 +01:00
|
|
|
// 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
|
2023-09-13 18:35:39 +02:00
|
|
|
}, (async function (accessToken, refreshToken, profile, done) {
|
2023-03-24 18:21:25 +01:00
|
|
|
// TODO: do we want to create the users which does not exist?
|
2023-09-13 18:35:39 +02:00
|
|
|
const user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase())
|
2023-04-14 20:26:29 +02:00
|
|
|
|
|
|
|
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: do we want to create the users which does not exist?
|
2023-09-13 18:35:39 +02:00
|
|
|
var user = Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase())
|
2023-04-14 20:26:29 +02:00
|
|
|
|
|
|
|
if (!user || !user.isActive) {
|
|
|
|
done(null, null)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
return done(null, user)
|
|
|
|
}).bind(this)))
|
2023-03-24 18:21:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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?
|
2023-03-24 18:31:58 +01:00
|
|
|
return cb(null, JSON.stringify({
|
2023-03-24 18:21:25 +01:00
|
|
|
"username": user.username,
|
|
|
|
"id": user.id,
|
2023-09-13 18:35:39 +02:00
|
|
|
"email": user.email,
|
2023-04-16 17:08:13 +02:00
|
|
|
}))
|
|
|
|
})
|
|
|
|
})
|
2023-03-24 18:21:25 +01:00
|
|
|
|
|
|
|
// define how to deseralize a user (use the username to get it from the database)
|
2023-03-24 18:31:58 +01:00
|
|
|
passport.deserializeUser((function (user, cb) {
|
2023-09-13 18:35:39 +02:00
|
|
|
process.nextTick((async function () {
|
2023-03-24 18:31:58 +01:00
|
|
|
const parsedUserInfo = JSON.parse(user)
|
2023-03-24 18:21:25 +01:00
|
|
|
// TODO: do the matching on username or better on id?
|
2023-09-13 18:35:39 +02:00
|
|
|
const dbUser = await Database.userModel.getUserByUsername(parsedUserInfo.username.toLowerCase())
|
2023-04-16 17:08:13 +02:00
|
|
|
return cb(null, dbUser)
|
|
|
|
}).bind(this))
|
|
|
|
}).bind(this))
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
|
2023-03-24 18:21:25 +01:00
|
|
|
/**
|
|
|
|
* Creates all (express) routes required for authentication.
|
|
|
|
* @param {express.Router} router
|
|
|
|
*/
|
|
|
|
initAuthRoutes(router) {
|
|
|
|
// Local strategy login route (takes username and password)
|
2023-04-16 17:08:13 +02:00
|
|
|
router.post('/login', passport.authenticate('local'),
|
2023-09-13 18:35:39 +02:00
|
|
|
(async function (req, res) {
|
2023-03-24 18:21:25 +01:00
|
|
|
// return the user login response json if the login was successfull
|
2023-09-13 18:35:39 +02:00
|
|
|
res.json(await this.getUserLoginResponsePayload(req.user))
|
2023-03-24 18:21:25 +01:00
|
|
|
}).bind(this)
|
|
|
|
)
|
|
|
|
|
|
|
|
// google-oauth20 strategy login route (this redirects to the google login)
|
2023-09-14 19:49:19 +02:00
|
|
|
router.get('/auth/google', (req, res, next) => {
|
|
|
|
const auth_func = passport.authenticate('google', { scope: ['email'] })
|
|
|
|
if (!req.query.callback || req.query.callback === "") {
|
|
|
|
res.status(400).send({
|
|
|
|
message: 'No callback parameter'
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
res.cookie('auth_cb', req.query.callback, {
|
|
|
|
maxAge: 120000 * 120, // Hack - this semms to be in UTC??
|
|
|
|
httpOnly: true
|
|
|
|
})
|
|
|
|
auth_func(req, res, next);
|
|
|
|
})
|
2023-03-24 18:21:25 +01:00
|
|
|
|
|
|
|
// google-oauth20 strategy callback route (this receives the token from google)
|
|
|
|
router.get('/auth/google/callback',
|
2023-04-16 17:08:13 +02:00
|
|
|
passport.authenticate('google'),
|
2023-09-13 18:35:39 +02:00
|
|
|
(async function (req, res) {
|
2023-03-24 18:21:25 +01:00
|
|
|
// return the user login response json if the login was successfull
|
2023-09-13 18:35:39 +02:00
|
|
|
var data_json = await this.getUserLoginResponsePayload(req.user)
|
|
|
|
// res.json(data_json)
|
2023-09-14 19:49:19 +02:00
|
|
|
// TODO: do we want to somehow limit the values for auth_cb?
|
|
|
|
if (req.cookies.auth_cb) {
|
|
|
|
res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`)
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
res.status(400).send("No callback or already expired")
|
|
|
|
}
|
2023-03-24 18:21:25 +01:00
|
|
|
}).bind(this)
|
|
|
|
)
|
|
|
|
|
2023-04-14 20:26:29 +02:00
|
|
|
// openid strategy login route (this redirects to the configured openid login provider)
|
2023-09-14 19:49:19 +02:00
|
|
|
router.get('/auth/openid', (req, res, next) => {
|
|
|
|
const auth_func = passport.authenticate('openidconnect')
|
|
|
|
if (!req.query.callback || req.query.callback === "") {
|
|
|
|
res.status(400).send({
|
|
|
|
message: 'No callback parameter'
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
res.cookie('auth_cb', req.query.callback, {
|
|
|
|
maxAge: 120000 * 120, // Hack - this semms to be in UTC??
|
|
|
|
httpOnly: true
|
|
|
|
})
|
|
|
|
auth_func(req, res, next);
|
|
|
|
})
|
2023-04-14 20:26:29 +02:00
|
|
|
|
|
|
|
// openid strategy callback route (this receives the token from the configured openid login provider)
|
|
|
|
router.get('/auth/openid/callback',
|
2023-04-16 17:08:13 +02:00
|
|
|
passport.authenticate('openidconnect'),
|
2023-09-13 18:35:39 +02:00
|
|
|
(async function (req, res) {
|
2023-04-14 20:26:29 +02:00
|
|
|
// return the user login response json if the login was successfull
|
2023-09-13 18:35:39 +02:00
|
|
|
var data_json = await this.getUserLoginResponsePayload(req.user)
|
|
|
|
// res.json(data_json)
|
2023-09-14 19:49:19 +02:00
|
|
|
if (req.cookies.auth_cb) {
|
|
|
|
res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}`)
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
res.status(400).send("No callback or already expired")
|
|
|
|
}
|
2023-04-14 20:26:29 +02:00
|
|
|
}).bind(this)
|
|
|
|
)
|
|
|
|
|
2023-03-24 18:21:25 +01:00
|
|
|
// Logout route
|
2023-04-16 17:08:13 +02:00
|
|
|
router.post('/logout', (req, res) => {
|
2023-03-24 18:21:25 +01:00
|
|
|
// TODO: invalidate possible JWTs
|
2023-04-16 17:08:13 +02:00
|
|
|
req.logout((err) => {
|
|
|
|
if (err) {
|
|
|
|
res.sendStatus(500)
|
|
|
|
} else {
|
|
|
|
res.sendStatus(200)
|
|
|
|
}
|
|
|
|
})
|
2023-03-24 18:21:25 +01:00
|
|
|
})
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
|
2023-03-24 18:21:25 +01:00
|
|
|
/**
|
|
|
|
* 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()) {
|
2021-08-18 00:01:11 +02:00
|
|
|
next()
|
2023-03-24 18:21:25 +01:00
|
|
|
} else {
|
|
|
|
// try JWT to authenticate
|
|
|
|
passport.authenticate("jwt")(req, res, next)
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-24 18:21:25 +01:00
|
|
|
/**
|
|
|
|
* Function to generate a jwt token for a given user.
|
|
|
|
* @param {Object} user
|
|
|
|
* @returns the token.
|
|
|
|
*/
|
|
|
|
generateAccessToken(user) {
|
2023-04-16 17:08:13 +02:00
|
|
|
return jwt.sign({ userId: user.id, username: user.username }, global.ServerSettings.tokenSecret)
|
2023-03-24 18:21:25 +01:00
|
|
|
}
|
|
|
|
|
2023-09-13 18:35:39 +02:00
|
|
|
/**
|
|
|
|
* Function to generate a jwt token for a given user.
|
|
|
|
* @param {string} token
|
|
|
|
* @returns the tokens data.
|
|
|
|
*/
|
|
|
|
static validateAccessToken(token) {
|
|
|
|
try {
|
|
|
|
return jwt.verify(token, global.ServerSettings.tokenSecret)
|
|
|
|
}
|
|
|
|
catch (err) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-24 18:21:25 +01:00
|
|
|
/**
|
|
|
|
* Generate a token for each user.
|
|
|
|
*/
|
2022-07-19 00:19:16 +02:00
|
|
|
async initTokenSecret() {
|
|
|
|
if (process.env.TOKEN_SECRET) { // User can supply their own token secret
|
2023-09-16 20:22:11 +02:00
|
|
|
global.ServerSettings.tokenSecret = process.env.TOKEN_SECRET
|
2022-07-19 00:19:16 +02:00
|
|
|
} else {
|
2023-09-16 20:22:11 +02:00
|
|
|
global.ServerSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64')
|
2022-07-19 00:19:16 +02:00
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
await Database.updateServerSettings()
|
2022-07-19 00:19:16 +02:00
|
|
|
|
|
|
|
// New token secret creation added in v2.1.0 so generate new API tokens for each user
|
2023-08-20 20:34:03 +02:00
|
|
|
const users = await Database.userModel.getOldUsers()
|
2023-07-22 22:32:20 +02:00
|
|
|
if (users.length) {
|
|
|
|
for (const user of users) {
|
2022-07-19 00:19:16 +02:00
|
|
|
user.token = await this.generateAccessToken({ userId: user.id, username: user.username })
|
|
|
|
}
|
2023-07-22 22:32:20 +02:00
|
|
|
await Database.updateBulkUsers(users)
|
2022-07-19 00:19:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-24 18:21:25 +01:00
|
|
|
/**
|
|
|
|
* 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) {
|
2023-09-13 18:35:39 +02:00
|
|
|
const user = Database.userModel.getUserByUsername(jwt_payload.username.toLowerCase())
|
2021-09-22 03:57:33 +02:00
|
|
|
|
2023-03-24 18:21:25 +01:00
|
|
|
if (!user || !user.isActive) {
|
|
|
|
done(null, null)
|
|
|
|
return
|
2021-09-22 03:57:33 +02:00
|
|
|
}
|
2023-03-24 18:21:25 +01:00
|
|
|
done(null, user)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-04-16 17:08:13 +02:00
|
|
|
* Checks if a username and password tuple is valid and the user active.
|
2023-03-24 18:21:25 +01:00
|
|
|
* @param {string} username
|
|
|
|
* @param {string} password
|
|
|
|
* @param {function} done
|
|
|
|
*/
|
2023-04-16 17:08:13 +02:00
|
|
|
async localAuthCheckUserPw(username, password, done) {
|
2023-09-13 18:35:39 +02:00
|
|
|
const user = Database.userModel.getUserByUsername(username.toLowerCase())
|
2021-09-22 03:57:33 +02:00
|
|
|
|
2023-03-24 18:21:25 +01:00
|
|
|
if (!user || !user.isActive) {
|
|
|
|
done(null, null)
|
|
|
|
return
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
|
2023-03-24 18:21:25 +01:00
|
|
|
// Check passwordless root user
|
|
|
|
if (user.id === 'root' && (!user.pash || user.pash === '')) {
|
|
|
|
if (password) {
|
|
|
|
done(null, null)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
done(null, user)
|
|
|
|
return
|
2021-09-11 02:55:02 +02:00
|
|
|
}
|
2023-03-24 18:21:25 +01:00
|
|
|
|
|
|
|
// Check password match
|
2023-04-16 17:08:13 +02:00
|
|
|
const compare = await bcrypt.compare(password, user.pash)
|
2023-03-24 18:21:25 +01:00
|
|
|
if (compare) {
|
|
|
|
done(null, user)
|
|
|
|
return
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
2023-03-24 18:21:25 +01:00
|
|
|
done(null, null)
|
|
|
|
return
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
|
2023-03-24 18:21:25 +01:00
|
|
|
/**
|
|
|
|
* Hashes a password with bcrypt.
|
|
|
|
* @param {string} password
|
|
|
|
* @returns {string} hash
|
|
|
|
*/
|
2021-08-18 00:01:11 +02:00
|
|
|
hashPass(password) {
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
bcrypt.hash(password, 8, (err, hash) => {
|
|
|
|
if (err) {
|
|
|
|
resolve(null)
|
|
|
|
} else {
|
|
|
|
resolve(hash)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-03-24 18:21:25 +01:00
|
|
|
/**
|
|
|
|
* Return the login info payload for a user.
|
|
|
|
* @param {string} username
|
|
|
|
* @returns {string} jsonPayload
|
|
|
|
*/
|
2023-09-13 18:35:39 +02:00
|
|
|
async getUserLoginResponsePayload(user) {
|
|
|
|
const libraryIds = await Database.libraryModel.getAllLibraryIds()
|
2022-04-30 00:43:46 +02:00
|
|
|
return {
|
|
|
|
user: user.toJSONForBrowser(),
|
2023-07-22 21:25:20 +02:00
|
|
|
userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
|
2023-07-05 01:14:44 +02:00
|
|
|
serverSettings: Database.serverSettings.toJSONForBrowser(),
|
|
|
|
ereaderDevices: Database.emailSettings.getEReaderDevices(user),
|
2022-05-21 18:21:03 +02:00
|
|
|
Source: global.Source
|
2022-04-30 00:43:46 +02:00
|
|
|
}
|
|
|
|
}
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
2023-03-24 18:21:25 +01:00
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
module.exports = Auth
|