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' )
2022-11-18 01:04:11 +01:00
const requestIp = require ( './libs/requestIp' )
2021-08-18 00:01:11 +02:00
const Logger = require ( './Logger' )
2023-07-05 01:14:44 +02:00
const Database = require ( './Database' )
2021-08-18 00:01:11 +02:00
class Auth {
2023-07-05 01:14:44 +02:00
constructor ( ) { }
2021-08-18 00:01:11 +02:00
cors ( req , res , next ) {
res . header ( 'Access-Control-Allow-Origin' , '*' )
res . header ( "Access-Control-Allow-Methods" , 'GET, POST, PATCH, PUT, DELETE, OPTIONS' )
2022-06-25 17:36:37 +02:00
res . header ( 'Access-Control-Allow-Headers' , '*' )
// TODO: Make sure allowing all headers is not a security concern. It is required for adding custom headers for SSO
// res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Range, Authorization")
2021-08-18 00:01:11 +02:00
res . header ( 'Access-Control-Allow-Credentials' , true )
if ( req . method === 'OPTIONS' ) {
res . sendStatus ( 200 )
} else {
next ( )
}
}
2022-07-19 00:19:16 +02:00
async initTokenSecret ( ) {
if ( process . env . TOKEN _SECRET ) { // User can supply their own token secret
Logger . debug ( ` [Auth] Setting token secret - using user passed in TOKEN_SECRET env var ` )
2023-07-05 01:14:44 +02:00
Database . serverSettings . tokenSecret = process . env . TOKEN _SECRET
2022-07-19 00:19:16 +02:00
} else {
Logger . debug ( ` [Auth] Setting token secret - using random bytes ` )
2023-07-05 01:14:44 +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-07-05 01:14:44 +02:00
if ( Database . users . length ) {
for ( const user of Database . users ) {
2022-07-19 00:19:16 +02:00
user . token = await this . generateAccessToken ( { userId : user . id , username : user . username } )
Logger . warn ( ` [Auth] User ${ user . username } api token has been updated using new token secret ` )
}
2023-07-05 01:14:44 +02:00
await Database . updateBulkUsers ( Database . users )
2022-07-19 00:19:16 +02:00
}
}
2021-08-18 00:01:11 +02:00
async authMiddleware ( req , res , next ) {
2021-09-22 03:57:33 +02:00
var token = null
// If using a get request, the token can be passed as a query string
if ( req . method === 'GET' && req . query && req . query . token ) {
token = req . query . token
} else {
const authHeader = req . headers [ 'authorization' ]
token = authHeader && authHeader . split ( ' ' ) [ 1 ]
}
2021-08-18 00:01:11 +02:00
if ( token == null ) {
2021-08-24 02:37:40 +02:00
Logger . error ( 'Api called without a token' , req . path )
2021-08-18 00:01:11 +02:00
return res . sendStatus ( 401 )
}
2023-07-05 01:14:44 +02:00
const user = await this . verifyToken ( token )
2021-08-18 00:01:11 +02:00
if ( ! user ) {
Logger . error ( 'Verify Token User Not Found' , token )
2021-09-11 02:55:02 +02:00
return res . sendStatus ( 404 )
}
if ( ! user . isActive ) {
Logger . error ( 'Verify Token User is disabled' , token , user . username )
2021-08-18 00:01:11 +02:00
return res . sendStatus ( 403 )
}
req . user = user
next ( )
}
hashPass ( password ) {
return new Promise ( ( resolve ) => {
bcrypt . hash ( password , 8 , ( err , hash ) => {
if ( err ) {
Logger . error ( 'Hash failed' , err )
resolve ( null )
} else {
resolve ( hash )
}
} )
} )
}
generateAccessToken ( payload ) {
2023-07-05 01:14:44 +02:00
return jwt . sign ( payload , Database . serverSettings . tokenSecret )
2021-08-18 00:01:11 +02:00
}
2021-11-13 02:43:16 +01:00
authenticateUser ( token ) {
return this . verifyToken ( token )
}
2021-08-18 00:01:11 +02:00
verifyToken ( token ) {
return new Promise ( ( resolve ) => {
2023-07-05 01:14:44 +02:00
jwt . verify ( token , Database . serverSettings . tokenSecret , ( err , payload ) => {
2021-08-23 21:08:54 +02:00
if ( ! payload || err ) {
Logger . error ( 'JWT Verify Token Failed' , err )
return resolve ( null )
}
2023-07-05 01:14:44 +02:00
const user = Database . users . find ( u => ( u . id === payload . userId || u . oldUserId === payload . userId ) && u . username === payload . username )
2021-08-18 00:01:11 +02:00
resolve ( user || null )
} )
} )
}
2022-12-31 17:59:12 +01:00
getUserLoginResponsePayload ( user ) {
2022-04-30 00:43:46 +02:00
return {
user : user . toJSONForBrowser ( ) ,
2023-07-05 01:14:44 +02:00
userDefaultLibraryId : user . getDefaultLibraryId ( Database . libraries ) ,
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
}
}
2022-12-31 17:59:12 +01:00
async login ( req , res ) {
2022-11-18 01:04:11 +01:00
const ipAddress = requestIp . getClientIp ( req )
2023-04-28 23:16:47 +02:00
const username = ( req . body . username || '' ) . toLowerCase ( )
const password = req . body . password || ''
2021-08-18 00:01:11 +02:00
2023-07-05 01:14:44 +02:00
const user = Database . users . find ( u => u . username . toLowerCase ( ) === username )
2021-08-18 00:01:11 +02:00
2023-04-28 23:16:47 +02:00
if ( ! user ? . isActive ) {
2022-11-18 01:04:11 +01:00
Logger . warn ( ` [Auth] Failed login attempt ${ req . rateLimit . current } of ${ req . rateLimit . limit } from ${ ipAddress } ` )
2021-09-29 17:16:38 +02:00
if ( req . rateLimit . remaining <= 2 ) {
2022-11-18 01:04:11 +01:00
Logger . error ( ` [Auth] Failed login attempt for username ${ username } from ip ${ ipAddress } . Attempts: ${ req . rateLimit . current } ` )
2021-09-29 17:16:38 +02:00
return res . status ( 401 ) . send ( ` Invalid user or password ( ${ req . rateLimit . remaining === 0 ? '1 attempt remaining' : ` ${ req . rateLimit . remaining + 1 } attempts remaining ` } ) ` )
}
return res . status ( 401 ) . send ( 'Invalid user or password' )
2021-09-11 02:55:02 +02:00
}
2021-08-18 00:01:11 +02:00
// Check passwordless root user
2023-07-05 01:14:44 +02:00
if ( user . type === 'root' && ( ! user . pash || user . pash === '' ) ) {
2021-08-18 00:01:11 +02:00
if ( password ) {
2021-09-29 17:16:38 +02:00
return res . status ( 401 ) . send ( 'Invalid root password (hint: there is none)' )
2021-08-18 00:01:11 +02:00
} else {
2023-04-28 23:16:47 +02:00
Logger . info ( ` [Auth] ${ user . username } logged in from ${ ipAddress } ` )
2022-12-31 17:59:12 +01:00
return res . json ( this . getUserLoginResponsePayload ( user ) )
2021-08-18 00:01:11 +02:00
}
}
// Check password match
2023-04-28 23:16:47 +02:00
const compare = await bcrypt . compare ( password , user . pash )
2021-08-18 00:01:11 +02:00
if ( compare ) {
2023-04-28 23:16:47 +02:00
Logger . info ( ` [Auth] ${ user . username } logged in from ${ ipAddress } ` )
2022-12-31 17:59:12 +01:00
res . json ( this . getUserLoginResponsePayload ( user ) )
2021-08-18 00:01:11 +02:00
} else {
2022-11-18 01:04:11 +01:00
Logger . warn ( ` [Auth] Failed login attempt ${ req . rateLimit . current } of ${ req . rateLimit . limit } from ${ ipAddress } ` )
2021-09-29 17:16:38 +02:00
if ( req . rateLimit . remaining <= 2 ) {
2022-11-18 01:04:11 +01:00
Logger . error ( ` [Auth] Failed login attempt for user ${ user . username } from ip ${ ipAddress } . Attempts: ${ req . rateLimit . current } ` )
2021-09-29 17:16:38 +02:00
return res . status ( 401 ) . send ( ` Invalid user or password ( ${ req . rateLimit . remaining === 0 ? '1 attempt remaining' : ` ${ req . rateLimit . remaining + 1 } attempts remaining ` } ) ` )
}
return res . status ( 401 ) . send ( 'Invalid user or password' )
2021-08-18 00:01:11 +02:00
}
}
2021-08-22 17:46:04 +02:00
comparePassword ( password , user ) {
if ( user . type === 'root' && ! password && ! user . pash ) return true
if ( ! password || ! user . pash ) return false
return bcrypt . compare ( password , user . pash )
}
async userChangePassword ( req , res ) {
var { password , newPassword } = req . body
newPassword = newPassword || ''
2023-07-05 01:14:44 +02:00
const matchingUser = Database . users . find ( u => u . id === req . user . id )
2021-08-18 00:01:11 +02:00
2021-08-22 17:46:04 +02:00
// Only root can have an empty password
if ( matchingUser . type !== 'root' && ! newPassword ) {
2021-08-18 00:01:11 +02:00
return res . json ( {
2021-08-22 17:46:04 +02:00
error : 'Invalid new password - Only root can have an empty password'
2021-08-18 00:01:11 +02:00
} )
}
2023-07-05 01:14:44 +02:00
const compare = await this . comparePassword ( password , matchingUser )
2021-08-22 17:46:04 +02:00
if ( ! compare ) {
return res . json ( {
error : 'Invalid password'
} )
2021-08-18 00:01:11 +02:00
}
2023-07-05 01:14:44 +02:00
let pw = ''
2021-08-22 17:46:04 +02:00
if ( newPassword ) {
pw = await this . hashPass ( newPassword )
2021-08-18 00:01:11 +02:00
if ( ! pw ) {
return res . json ( {
error : 'Hash failed'
} )
}
}
2021-08-22 17:46:04 +02:00
matchingUser . pash = pw
2023-07-05 01:14:44 +02:00
const success = await Database . updateUser ( matchingUser )
2021-08-22 17:46:04 +02:00
if ( success ) {
2021-08-18 00:01:11 +02:00
res . json ( {
2021-08-22 17:46:04 +02:00
success : true
2021-08-18 00:01:11 +02:00
} )
} else {
res . json ( {
2021-08-22 17:46:04 +02:00
error : 'Unknown error'
2021-08-18 00:01:11 +02:00
} )
}
}
}
module . exports = Auth