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' )
class Auth {
constructor ( db ) {
this . db = db
this . user = null
}
get username ( ) {
return this . user ? this . user . username : 'nobody'
}
get users ( ) {
return this . db . users
}
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 ` )
this . db . serverSettings . tokenSecret = process . env . TOKEN _SECRET
} else {
Logger . debug ( ` [Auth] Setting token secret - using random bytes ` )
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 } )
Logger . warn ( ` [Auth] User ${ user . username } api token has been updated using new token secret ` )
}
await this . db . updateEntities ( 'user' , this . db . users )
}
}
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 )
}
var user = await this . verifyToken ( token )
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 ) {
2022-07-19 00:19:16 +02:00
return jwt . sign ( payload , global . 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 ) => {
2022-07-19 00:19:16 +02:00
jwt . verify ( token , global . 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 )
}
2022-12-01 00:32:59 +01:00
const user = this . users . find ( u => u . id === payload . userId && u . username === payload . username )
2021-08-18 00:01:11 +02:00
resolve ( user || null )
} )
} )
}
2022-08-06 02:23:18 +02:00
getUserLoginResponsePayload ( user , feeds ) {
2022-04-30 00:43:46 +02:00
return {
user : user . toJSONForBrowser ( ) ,
userDefaultLibraryId : user . getDefaultLibraryId ( this . db . libraries ) ,
2022-07-19 00:19:16 +02:00
serverSettings : this . db . serverSettings . toJSONForBrowser ( ) ,
2022-08-06 02:23:18 +02:00
feeds ,
2022-05-21 18:21:03 +02:00
Source : global . Source
2022-04-30 00:43:46 +02:00
}
}
2022-08-06 02:23:18 +02:00
async login ( req , res , feeds ) {
2022-11-18 01:04:11 +01:00
const ipAddress = requestIp . getClientIp ( req )
2021-10-21 01:54:05 +02:00
var username = ( req . body . username || '' ) . toLowerCase ( )
2021-08-18 00:01:11 +02:00
var password = req . body . password || ''
2021-10-21 01:54:05 +02:00
var user = this . users . find ( u => u . username . toLowerCase ( ) === username )
2021-08-18 00:01:11 +02:00
2021-09-29 17:16:38 +02:00
if ( ! user || ! 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
if ( user . id === 'root' && ( ! user . pash || user . pash === '' ) ) {
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 {
2022-08-06 02:23:18 +02:00
return res . json ( this . getUserLoginResponsePayload ( user , feeds ) )
2021-08-18 00:01:11 +02:00
}
}
// Check password match
var compare = await bcrypt . compare ( password , user . pash )
if ( compare ) {
2022-08-06 02:23:18 +02:00
res . json ( this . getUserLoginResponsePayload ( user , feeds ) )
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-09-29 17:16:38 +02:00
// Not in use now
lockUser ( user ) {
user . isLocked = true
return this . db . updateEntity ( 'user' , user ) . catch ( ( error ) => {
Logger . error ( '[Auth] Failed to lock user' , user . username , error )
return false
} )
}
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 || ''
var matchingUser = this . 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
} )
}
2021-08-22 17:46:04 +02:00
var compare = await this . comparePassword ( password , matchingUser )
if ( ! compare ) {
return res . json ( {
error : 'Invalid password'
} )
2021-08-18 00:01:11 +02:00
}
2021-08-22 17:46:04 +02:00
var pw = ''
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
var success = await this . db . updateEntity ( 'user' , matchingUser )
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