audiobookshelf/server/Auth.js

399 lines
13 KiB
JavaScript
Raw Normal View History

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')
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
}
static cors(req, res, next) {
res.header('Access-Control-Allow-Origin', '*')
res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
res.header('Access-Control-Allow-Headers', '*')
res.header('Access-Control-Allow-Credentials', true)
if (req.method === 'OPTIONS') {
res.sendStatus(200)
} else {
next()
}
}
2023-03-24 18:21:25 +01:00
/**
2023-09-20 19:37:55 +02:00
* Inializes all passportjs strategies and other passportjs ralated initialization.
2023-03-24 18:21:25 +01:00
*/
2023-09-16 20:42:48 +02:00
async initPassportJs() {
2023-09-20 19:37:55 +02:00
// Check if we should load the local strategy (username + password login)
2023-03-24 18:21:25 +01:00
if (global.ServerSettings.authActiveAuthMethods.includes("local")) {
passport.use(new LocalStrategy(this.localAuthCheckUserPw.bind(this)))
}
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-20 19:37:55 +02:00
// get user by email
2023-09-13 18:35:39 +02:00
const user = await Database.userModel.getUserByEmail(profile.emails[0].value.toLowerCase())
if (!user || !user.isActive) {
2023-09-20 19:37:55 +02:00
// deny login
done(null, null)
return
}
2023-09-20 19:37:55 +02:00
// permit login
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: '/auth/openid/callback',
scope: ["openid", "email", "profile"],
skipUserProfile: false
}, async (issuer, profile, done) => {
// TODO: do we want to create the users which does not exist?
2023-09-20 19:37:55 +02:00
const user = await Database.userModel.getUserByUsername(profile.username)
if (!user?.isActive) {
// deny login
done(null, null)
return
}
// permit login
return done(null, user)
}))
2023-03-24 18:21:25 +01:00
}
2023-09-16 20:42:48 +02:00
2023-03-24 18:21:25 +01:00
// Load the JwtStrategy (always) -> for bearer token auth
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]),
secretOrKey: Database.serverSettings.tokenSecret
2023-03-24 18:21:25 +01:00
}, this.jwtAuthCheck.bind(this)))
// define how to seralize a user (to be put into the session)
passport.serializeUser(function (user, cb) {
process.nextTick(function () {
2023-09-20 19:37:55 +02:00
// only store id to session
return cb(null, JSON.stringify({
2023-03-24 18:21:25 +01:00
"id": user.id,
}))
})
})
2023-03-24 18:21:25 +01:00
2023-09-20 19:37:55 +02:00
// define how to deseralize a user (use the ID to get it from the database)
passport.deserializeUser((function (user, cb) {
2023-09-13 18:35:39 +02:00
process.nextTick((async function () {
const parsedUserInfo = JSON.parse(user)
2023-09-20 19:37:55 +02:00
// load the user by ID that is stored in the session
const dbUser = await Database.userModel.getUserById(parsedUserInfo.id)
return cb(null, dbUser)
}).bind(this))
}).bind(this))
2021-08-18 00:01:11 +02:00
}
2023-09-17 19:42:42 +02:00
/**
* Stores the client's choice how the login callback should happen in temp cookies
*
* @param {import('express').Request} req
* @param {import('express').Response} res
2023-09-17 19:42:42 +02:00
*/
paramsToCookies(req, res) {
if (req.query.isRest?.toLowerCase() == "true") {
2023-09-20 19:37:55 +02:00
// store the isRest flag to the is_rest cookie
2023-09-17 19:42:42 +02:00
res.cookie('is_rest', req.query.isRest.toLowerCase(), {
2023-09-20 19:48:57 +02:00
maxAge: 120000, // 2 min
2023-09-17 19:42:42 +02:00
httpOnly: true
})
} else {
2023-09-20 19:37:55 +02:00
// no isRest-flag set -> set is_rest cookie to false
2023-09-17 19:42:42 +02:00
res.cookie('is_rest', "false", {
2023-09-20 19:48:57 +02:00
maxAge: 120000, // 2 min
2023-09-17 19:42:42 +02:00
httpOnly: true
})
2023-09-20 19:37:55 +02:00
// persist state if passed in
if (req.query.state) {
res.cookie('auth_state', req.query.state, {
maxAge: 120000, // 2 min
httpOnly: true
})
}
const callback = req.query.redirect_uri || req.query.callback
2023-09-20 19:37:55 +02:00
// check if we are missing a callback parameter - we need one if isRest=false
if (!callback) {
2023-09-17 19:42:42 +02:00
res.status(400).send({
message: 'No callback parameter'
})
return
}
2023-09-20 19:37:55 +02:00
// store the callback url to the auth_cb cookie
res.cookie('auth_cb', callback, {
2023-09-20 19:48:57 +02:00
maxAge: 120000, // 2 min
2023-09-17 19:42:42 +02:00
httpOnly: true
})
}
}
/**
* Informs the client in the right mode about a successfull login and the token
* (clients choise is restored from cookies).
*
* @param {import('express').Request} req
* @param {import('express').Response} res
2023-09-17 19:42:42 +02:00
*/
async handleLoginSuccessBasedOnCookie(req, res) {
2023-09-20 19:37:55 +02:00
// get userLogin json (information about the user, server and the session)
2023-09-17 19:42:42 +02:00
const data_json = await this.getUserLoginResponsePayload(req.user)
if (req.cookies.is_rest === 'true') {
2023-09-17 19:42:42 +02:00
// REST request - send data
res.json(data_json)
} else {
2023-09-17 19:42:42 +02:00
// UI request -> check if we have a callback url
// TODO: do we want to somehow limit the values for auth_cb?
if (req.cookies.auth_cb) {
let stateQuery = req.cookies.auth_state ? `&state=${req.cookies.auth_state}` : ''
2023-09-20 19:37:55 +02:00
// UI request -> redirect to auth_cb url and send the jwt token as parameter
res.redirect(302, `${req.cookies.auth_cb}?setToken=${data_json.user.token}${stateQuery}`)
} else {
res.status(400).send('No callback or already expired')
2023-09-17 19:42:42 +02:00
}
}
}
2023-03-24 18:21:25 +01:00
/**
* Creates all (express) routes required for authentication.
*
* @param {import('express').Router} router
2023-03-24 18:21:25 +01:00
*/
2023-09-20 19:37:55 +02:00
async initAuthRoutes(router) {
2023-03-24 18:21:25 +01:00
// Local strategy login route (takes username and password)
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))
})
2023-03-24 18:21:25 +01:00
// google-oauth20 strategy login route (this redirects to the google login)
router.get('/auth/google', (req, res, next) => {
const auth_func = passport.authenticate('google', { scope: ['email'] })
2023-09-20 19:37:55 +02:00
// params (isRest, callback) to a cookie that will be send to the client
2023-09-17 19:42:42 +02:00
this.paramsToCookies(req, res)
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',
passport.authenticate('google'),
2023-09-20 19:37:55 +02:00
// on a successfull login: read the cookies and react like the client requested (callback or json)
2023-09-17 19:42:42 +02:00
this.handleLoginSuccessBasedOnCookie.bind(this)
2023-03-24 18:21:25 +01:00
)
// openid strategy login route (this redirects to the configured openid login provider)
router.get('/auth/openid', (req, res, next) => {
const auth_func = passport.authenticate('openidconnect')
2023-09-20 19:37:55 +02:00
// params (isRest, callback) to a cookie that will be send to the client
2023-09-17 19:42:42 +02:00
this.paramsToCookies(req, res)
auth_func(req, res, next)
})
// openid strategy callback route (this receives the token from the configured openid login provider)
router.get('/auth/openid/callback',
passport.authenticate('openidconnect'),
2023-09-20 19:37:55 +02:00
// on a successfull login: read the cookies and react like the client requested (callback or json)
this.handleLoginSuccessBasedOnCookie.bind(this))
2023-03-24 18:21:25 +01:00
// Logout route
router.post('/logout', (req, res) => {
2023-03-24 18:21:25 +01:00
// TODO: invalidate possible JWTs
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 {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
2023-03-24 18:21:25 +01:00
*/
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
*
2023-03-24 18:21:25 +01:00
* @param {Object} user
* @returns {string} token
2023-03-24 18:21:25 +01:00
*/
generateAccessToken(user) {
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 validate a jwt token for a given user
*
2023-09-13 18:35:39 +02:00
* @param {string} token
* @returns {Object} tokens data
2023-09-13 18:35:39 +02:00
*/
static validateAccessToken(token) {
try {
return jwt.verify(token, global.ServerSettings.tokenSecret)
}
catch (err) {
return null
}
}
2023-03-24 18:21:25 +01:00
/**
2023-09-20 19:37:55 +02:00
* Generate a token which is used to encrpt/protect the jwts.
2023-03-24 18:21:25 +01:00
*/
2022-07-19 00:19:16 +02:00
async initTokenSecret() {
if (process.env.TOKEN_SECRET) { // User can supply their own token secret
2023-09-23 20:42:28 +02:00
Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET
2022-07-19 00:19:16 +02:00
} else {
2023-09-23 20:42:28 +02:00
Database.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
*/
2023-09-20 20:06:16 +02:00
async jwtAuthCheck(jwt_payload, done) {
2023-09-20 19:37:55 +02:00
// load user by id from the jwt token
const user = await Database.userModel.getUserByIdOrOldId(jwt_payload.userId)
if (!user?.isActive) {
2023-09-20 19:37:55 +02:00
// deny login
2023-03-24 18:21:25 +01:00
done(null, null)
return
}
2023-09-20 19:37:55 +02:00
// approve login
2023-03-24 18:21:25 +01:00
done(null, user)
return
}
/**
* 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
*/
async localAuthCheckUserPw(username, password, done) {
2023-09-20 19:37:55 +02:00
// Load the user given it's username
2023-09-16 21:45:04 +02:00
const user = await Database.userModel.getUserByUsername(username.toLowerCase())
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.type === 'root' && (!user.pash || user.pash === '')) {
2023-03-24 18:21:25 +01:00
if (password) {
2023-09-20 19:37:55 +02:00
// deny login
2023-03-24 18:21:25 +01:00
done(null, null)
return
}
2023-09-20 19:37:55 +02:00
// approve login
2023-03-24 18:21:25 +01:00
done(null, user)
return
}
2023-03-24 18:21:25 +01:00
// Check password match
const compare = await bcrypt.compare(password, user.pash)
2023-03-24 18:21:25 +01:00
if (compare) {
2023-09-20 19:37:55 +02:00
// approve login
2023-03-24 18:21:25 +01:00
done(null, user)
return
2021-08-18 00:01:11 +02:00
}
2023-09-20 19:37:55 +02:00
// deny login
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 {Object} user
* @returns {Promise<Object>} jsonPayload
2023-03-24 18:21:25 +01:00
*/
2023-09-13 18:35:39 +02:00
async getUserLoginResponsePayload(user) {
const libraryIds = await Database.libraryModel.getAllLibraryIds()
return {
user: user.toJSONForBrowser(),
userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
2023-07-05 01:14:44 +02:00
serverSettings: Database.serverSettings.toJSONForBrowser(),
ereaderDevices: Database.emailSettings.getEReaderDevices(user),
Source: global.Source
}
}
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