mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-03-20 02:17:37 +01:00
Update jsdocs and auto-formatting
This commit is contained in:
parent
15c6fce648
commit
eca51457b7
300
server/Auth.js
300
server/Auth.js
@ -13,7 +13,6 @@ const Logger = require('./Logger')
|
|||||||
* @class Class for handling all the authentication related functionality.
|
* @class Class for handling all the authentication related functionality.
|
||||||
*/
|
*/
|
||||||
class Auth {
|
class Auth {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Map of openId sessions indexed by oauth2 state-variable
|
// Map of openId sessions indexed by oauth2 state-variable
|
||||||
this.openIdAuthSession = new Map()
|
this.openIdAuthSession = new Map()
|
||||||
@ -24,40 +23,52 @@ class Auth {
|
|||||||
*/
|
*/
|
||||||
async initPassportJs() {
|
async initPassportJs() {
|
||||||
// Check if we should load the local strategy (username + password login)
|
// Check if we should load the local strategy (username + password login)
|
||||||
if (global.ServerSettings.authActiveAuthMethods.includes("local")) {
|
if (global.ServerSettings.authActiveAuthMethods.includes('local')) {
|
||||||
this.initAuthStrategyPassword()
|
this.initAuthStrategyPassword()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we should load the openid strategy
|
// Check if we should load the openid strategy
|
||||||
if (global.ServerSettings.authActiveAuthMethods.includes("openid")) {
|
if (global.ServerSettings.authActiveAuthMethods.includes('openid')) {
|
||||||
this.initAuthStrategyOpenID()
|
this.initAuthStrategyOpenID()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the JwtStrategy (always) -> for bearer token auth
|
// Load the JwtStrategy (always) -> for bearer token auth
|
||||||
passport.use(new JwtStrategy({
|
passport.use(
|
||||||
jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]),
|
new JwtStrategy(
|
||||||
secretOrKey: Database.serverSettings.tokenSecret
|
{
|
||||||
}, this.jwtAuthCheck.bind(this)))
|
jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]),
|
||||||
|
secretOrKey: Database.serverSettings.tokenSecret
|
||||||
|
},
|
||||||
|
this.jwtAuthCheck.bind(this)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// define how to seralize a user (to be put into the session)
|
// define how to seralize a user (to be put into the session)
|
||||||
passport.serializeUser(function (user, cb) {
|
passport.serializeUser(function (user, cb) {
|
||||||
process.nextTick(function () {
|
process.nextTick(function () {
|
||||||
// only store id to session
|
// only store id to session
|
||||||
return cb(null, JSON.stringify({
|
return cb(
|
||||||
id: user.id,
|
null,
|
||||||
}))
|
JSON.stringify({
|
||||||
|
id: user.id
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// define how to deseralize a user (use the ID to get it from the database)
|
// define how to deseralize a user (use the ID to get it from the database)
|
||||||
passport.deserializeUser((function (user, cb) {
|
passport.deserializeUser(
|
||||||
process.nextTick((async function () {
|
function (user, cb) {
|
||||||
const parsedUserInfo = JSON.parse(user)
|
process.nextTick(
|
||||||
// load the user by ID that is stored in the session
|
async function () {
|
||||||
const dbUser = await Database.userModel.getUserById(parsedUserInfo.id)
|
const parsedUserInfo = JSON.parse(user)
|
||||||
return cb(null, dbUser)
|
// load the user by ID that is stored in the session
|
||||||
}).bind(this))
|
const dbUser = await Database.userModel.getUserById(parsedUserInfo.id)
|
||||||
}).bind(this))
|
return cb(null, dbUser)
|
||||||
|
}.bind(this)
|
||||||
|
)
|
||||||
|
}.bind(this)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,43 +103,49 @@ class Auth {
|
|||||||
client_secret: global.ServerSettings.authOpenIDClientSecret,
|
client_secret: global.ServerSettings.authOpenIDClientSecret,
|
||||||
id_token_signed_response_alg: global.ServerSettings.authOpenIDTokenSigningAlgorithm
|
id_token_signed_response_alg: global.ServerSettings.authOpenIDTokenSigningAlgorithm
|
||||||
})
|
})
|
||||||
passport.use('openid-client', new OpenIDClient.Strategy({
|
passport.use(
|
||||||
client: openIdClient,
|
'openid-client',
|
||||||
params: {
|
new OpenIDClient.Strategy(
|
||||||
redirect_uri: '/auth/openid/callback',
|
{
|
||||||
scope: 'openid profile email'
|
client: openIdClient,
|
||||||
}
|
params: {
|
||||||
}, async (tokenset, userinfo, done) => {
|
redirect_uri: '/auth/openid/callback',
|
||||||
try {
|
scope: 'openid profile email'
|
||||||
Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2))
|
}
|
||||||
|
},
|
||||||
|
async (tokenset, userinfo, done) => {
|
||||||
|
try {
|
||||||
|
Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2))
|
||||||
|
|
||||||
if (!userinfo.sub) {
|
if (!userinfo.sub) {
|
||||||
throw new Error('Invalid userinfo, no sub')
|
throw new Error('Invalid userinfo, no sub')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.validateGroupClaim(userinfo)) {
|
||||||
|
throw new Error(`Group claim ${Database.serverSettings.authOpenIDGroupClaim} not found or empty in userinfo`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await this.findOrCreateUser(userinfo)
|
||||||
|
|
||||||
|
if (!user?.isActive) {
|
||||||
|
throw new Error('User not active or not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.setUserGroup(user, userinfo)
|
||||||
|
await this.updateUserPermissions(user, userinfo)
|
||||||
|
|
||||||
|
// We also have to save the id_token for later (used for logout) because we cannot set cookies here
|
||||||
|
user.openid_id_token = tokenset.id_token
|
||||||
|
|
||||||
|
return done(null, user)
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[Auth] openid callback error: ${error?.message}\n${error?.stack}`)
|
||||||
|
|
||||||
|
return done(null, null, 'Unauthorized')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
if (!this.validateGroupClaim(userinfo)) {
|
)
|
||||||
throw new Error(`Group claim ${Database.serverSettings.authOpenIDGroupClaim} not found or empty in userinfo`)
|
|
||||||
}
|
|
||||||
|
|
||||||
let user = await this.findOrCreateUser(userinfo)
|
|
||||||
|
|
||||||
if (!user?.isActive) {
|
|
||||||
throw new Error('User not active or not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.setUserGroup(user, userinfo)
|
|
||||||
await this.updateUserPermissions(user, userinfo)
|
|
||||||
|
|
||||||
// We also have to save the id_token for later (used for logout) because we cannot set cookies here
|
|
||||||
user.openid_id_token = tokenset.id_token
|
|
||||||
|
|
||||||
return done(null, user)
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(`[Auth] openid callback error: ${error?.message}\n${error?.stack}`)
|
|
||||||
|
|
||||||
return done(null, null, 'Unauthorized')
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -181,7 +198,6 @@ class Auth {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
user = await Database.userModel.getUserByUsername(username)
|
user = await Database.userModel.getUserByUsername(username)
|
||||||
|
|
||||||
if (user?.authOpenIDSub) {
|
if (user?.authOpenIDSub) {
|
||||||
@ -220,7 +236,8 @@ class Auth {
|
|||||||
*/
|
*/
|
||||||
validateGroupClaim(userinfo) {
|
validateGroupClaim(userinfo) {
|
||||||
const groupClaimName = Database.serverSettings.authOpenIDGroupClaim
|
const groupClaimName = Database.serverSettings.authOpenIDGroupClaim
|
||||||
if (!groupClaimName) // Allow no group claim when configured like this
|
if (!groupClaimName)
|
||||||
|
// Allow no group claim when configured like this
|
||||||
return true
|
return true
|
||||||
|
|
||||||
// If configured it must exist in userinfo
|
// If configured it must exist in userinfo
|
||||||
@ -238,16 +255,16 @@ class Auth {
|
|||||||
*/
|
*/
|
||||||
async setUserGroup(user, userinfo) {
|
async setUserGroup(user, userinfo) {
|
||||||
const groupClaimName = Database.serverSettings.authOpenIDGroupClaim
|
const groupClaimName = Database.serverSettings.authOpenIDGroupClaim
|
||||||
if (!groupClaimName) // No group claim configured, don't set anything
|
if (!groupClaimName)
|
||||||
|
// No group claim configured, don't set anything
|
||||||
return
|
return
|
||||||
|
|
||||||
if (!userinfo[groupClaimName])
|
if (!userinfo[groupClaimName]) throw new Error(`Group claim ${groupClaimName} not found in userinfo`)
|
||||||
throw new Error(`Group claim ${groupClaimName} not found in userinfo`)
|
|
||||||
|
|
||||||
const groupsList = userinfo[groupClaimName].map(group => group.toLowerCase())
|
const groupsList = userinfo[groupClaimName].map((group) => group.toLowerCase())
|
||||||
const rolesInOrderOfPriority = ['admin', 'user', 'guest']
|
const rolesInOrderOfPriority = ['admin', 'user', 'guest']
|
||||||
|
|
||||||
let userType = rolesInOrderOfPriority.find(role => groupsList.includes(role))
|
let userType = rolesInOrderOfPriority.find((role) => groupsList.includes(role))
|
||||||
if (userType) {
|
if (userType) {
|
||||||
if (user.type === 'root') {
|
if (user.type === 'root') {
|
||||||
// Check OpenID Group
|
// Check OpenID Group
|
||||||
@ -277,15 +294,14 @@ class Auth {
|
|||||||
*/
|
*/
|
||||||
async updateUserPermissions(user, userinfo) {
|
async updateUserPermissions(user, userinfo) {
|
||||||
const absPermissionsClaim = Database.serverSettings.authOpenIDAdvancedPermsClaim
|
const absPermissionsClaim = Database.serverSettings.authOpenIDAdvancedPermsClaim
|
||||||
if (!absPermissionsClaim) // No advanced permissions claim configured, don't set anything
|
if (!absPermissionsClaim)
|
||||||
|
// No advanced permissions claim configured, don't set anything
|
||||||
return
|
return
|
||||||
|
|
||||||
if (user.type === 'admin' || user.type === 'root')
|
if (user.type === 'admin' || user.type === 'root') return
|
||||||
return
|
|
||||||
|
|
||||||
const absPermissions = userinfo[absPermissionsClaim]
|
const absPermissions = userinfo[absPermissionsClaim]
|
||||||
if (!absPermissions)
|
if (!absPermissions) throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`)
|
||||||
throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`)
|
|
||||||
|
|
||||||
if (user.updatePermissionsFromExternalJSON(absPermissions)) {
|
if (user.updatePermissionsFromExternalJSON(absPermissions)) {
|
||||||
Logger.info(`[Auth] openid callback: Updating advanced perms for user "${user.username}" using "${JSON.stringify(absPermissions)}"`)
|
Logger.info(`[Auth] openid callback: Updating advanced perms for user "${user.username}" using "${JSON.stringify(absPermissions)}"`)
|
||||||
@ -422,7 +438,7 @@ class Auth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate a state on web flow or if no state supplied
|
// Generate a state on web flow or if no state supplied
|
||||||
const state = (!isMobileFlow || !req.query.state) ? OpenIDClient.generators.random() : req.query.state
|
const state = !isMobileFlow || !req.query.state ? OpenIDClient.generators.random() : req.query.state
|
||||||
|
|
||||||
// Redirect URL for the SSO provider
|
// Redirect URL for the SSO provider
|
||||||
let redirectUri
|
let redirectUri
|
||||||
@ -508,8 +524,7 @@ class Auth {
|
|||||||
|
|
||||||
function isValidRedirectUri(uri) {
|
function isValidRedirectUri(uri) {
|
||||||
// Check if the redirect_uri is in the whitelist
|
// Check if the redirect_uri is in the whitelist
|
||||||
return Database.serverSettings.authOpenIDMobileRedirectURIs.includes(uri) ||
|
return Database.serverSettings.authOpenIDMobileRedirectURIs.includes(uri) || (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')
|
||||||
(Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -545,72 +560,74 @@ class Auth {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// openid strategy callback route (this receives the token from the configured openid login provider)
|
// openid strategy callback route (this receives the token from the configured openid login provider)
|
||||||
router.get('/auth/openid/callback', (req, res, next) => {
|
router.get(
|
||||||
const oidcStrategy = passport._strategy('openid-client')
|
'/auth/openid/callback',
|
||||||
const sessionKey = oidcStrategy._key
|
(req, res, next) => {
|
||||||
|
const oidcStrategy = passport._strategy('openid-client')
|
||||||
|
const sessionKey = oidcStrategy._key
|
||||||
|
|
||||||
if (!req.session[sessionKey]) {
|
if (!req.session[sessionKey]) {
|
||||||
return res.status(400).send('No session')
|
return res.status(400).send('No session')
|
||||||
}
|
|
||||||
|
|
||||||
// If the client sends us a code_verifier, we will tell passport to use this to send this in the token request
|
|
||||||
// The code_verifier will be validated by the oauth2 provider by comparing it to the code_challenge in the first request
|
|
||||||
// Crucial for API/Mobile clients
|
|
||||||
if (req.query.code_verifier) {
|
|
||||||
req.session[sessionKey].code_verifier = req.query.code_verifier
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAuthError(isMobile, errorCode, errorMessage, logMessage, response) {
|
|
||||||
Logger.error(JSON.stringify(logMessage, null, 2))
|
|
||||||
if (response) {
|
|
||||||
// Depending on the error, it can also have a body
|
|
||||||
// We also log the request header the passport plugin sents for the URL
|
|
||||||
const header = response.req?._header.replace(/Authorization: [^\r\n]*/i, 'Authorization: REDACTED')
|
|
||||||
Logger.debug(header + '\n' + JSON.stringify(response.body, null, 2))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMobile) {
|
// If the client sends us a code_verifier, we will tell passport to use this to send this in the token request
|
||||||
return res.status(errorCode).send(errorMessage)
|
// The code_verifier will be validated by the oauth2 provider by comparing it to the code_challenge in the first request
|
||||||
} else {
|
// Crucial for API/Mobile clients
|
||||||
return res.redirect(`/login?error=${encodeURIComponent(errorMessage)}&autoLaunch=0`)
|
if (req.query.code_verifier) {
|
||||||
|
req.session[sessionKey].code_verifier = req.query.code_verifier
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function passportCallback(req, res, next) {
|
function handleAuthError(isMobile, errorCode, errorMessage, logMessage, response) {
|
||||||
return (err, user, info) => {
|
Logger.error(JSON.stringify(logMessage, null, 2))
|
||||||
const isMobile = req.session[sessionKey]?.mobile === true
|
if (response) {
|
||||||
if (err) {
|
// Depending on the error, it can also have a body
|
||||||
return handleAuthError(isMobile, 500, 'Error in callback', `[Auth] Error in openid callback - ${err}`, err?.response)
|
// We also log the request header the passport plugin sents for the URL
|
||||||
|
const header = response.req?._header.replace(/Authorization: [^\r\n]*/i, 'Authorization: REDACTED')
|
||||||
|
Logger.debug(header + '\n' + JSON.stringify(response.body, null, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (isMobile) {
|
||||||
// Info usually contains the error message from the SSO provider
|
return res.status(errorCode).send(errorMessage)
|
||||||
return handleAuthError(isMobile, 401, 'Unauthorized', `[Auth] No data in openid callback - ${info}`, info?.response)
|
} else {
|
||||||
|
return res.redirect(`/login?error=${encodeURIComponent(errorMessage)}&autoLaunch=0`)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
req.logIn(user, (loginError) => {
|
function passportCallback(req, res, next) {
|
||||||
if (loginError) {
|
return (err, user, info) => {
|
||||||
return handleAuthError(isMobile, 500, 'Error during login', `[Auth] Error in openid callback: ${loginError}`)
|
const isMobile = req.session[sessionKey]?.mobile === true
|
||||||
|
if (err) {
|
||||||
|
return handleAuthError(isMobile, 500, 'Error in callback', `[Auth] Error in openid callback - ${err}`, err?.response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The id_token does not provide access to the user, but is used to identify the user to the SSO provider
|
if (!user) {
|
||||||
// instead it containts a JWT with userinfo like user email, username, etc.
|
// Info usually contains the error message from the SSO provider
|
||||||
// the client will get to know it anyway in the logout url according to the oauth2 spec
|
return handleAuthError(isMobile, 401, 'Unauthorized', `[Auth] No data in openid callback - ${info}`, info?.response)
|
||||||
// so it is safe to send it to the client, but we use strict settings
|
}
|
||||||
res.cookie('openid_id_token', user.openid_id_token, { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true, secure: true, sameSite: 'Strict' })
|
|
||||||
next()
|
req.logIn(user, (loginError) => {
|
||||||
})
|
if (loginError) {
|
||||||
|
return handleAuthError(isMobile, 500, 'Error during login', `[Auth] Error in openid callback: ${loginError}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The id_token does not provide access to the user, but is used to identify the user to the SSO provider
|
||||||
|
// instead it containts a JWT with userinfo like user email, username, etc.
|
||||||
|
// the client will get to know it anyway in the logout url according to the oauth2 spec
|
||||||
|
// so it is safe to send it to the client, but we use strict settings
|
||||||
|
res.cookie('openid_id_token', user.openid_id_token, { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true, secure: true, sameSite: 'Strict' })
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request
|
||||||
// While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request
|
// We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided
|
||||||
// We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided
|
// We set it here again because the passport param can change between requests
|
||||||
// We set it here again because the passport param can change between requests
|
return passport.authenticate('openid-client', { redirect_uri: req.session[sessionKey].sso_redirect_uri }, passportCallback(req, res, next))(req, res, next)
|
||||||
return passport.authenticate('openid-client', { redirect_uri: req.session[sessionKey].sso_redirect_uri }, passportCallback(req, res, next))(req, res, next)
|
},
|
||||||
},
|
|
||||||
// on a successfull login: read the cookies and react like the client requested (callback or json)
|
// on a successfull login: read the cookies and react like the client requested (callback or json)
|
||||||
this.handleLoginSuccessBasedOnCookie.bind(this))
|
this.handleLoginSuccessBasedOnCookie.bind(this)
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper route used to auto-populate the openid URLs in config/authentication
|
* Helper route used to auto-populate the openid URLs in config/authentication
|
||||||
@ -625,7 +642,7 @@ class Auth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!req.query.issuer) {
|
if (!req.query.issuer) {
|
||||||
return res.status(400).send('Invalid request. Query param \'issuer\' is required')
|
return res.status(400).send("Invalid request. Query param 'issuer' is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip trailing slash
|
// Strip trailing slash
|
||||||
@ -641,23 +658,26 @@ class Auth {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[Auth] Failed to get openid configuration. Invalid URL "${configUrl}"`, error)
|
Logger.error(`[Auth] Failed to get openid configuration. Invalid URL "${configUrl}"`, error)
|
||||||
return res.status(400).send('Invalid request. Query param \'issuer\' is invalid')
|
return res.status(400).send("Invalid request. Query param 'issuer' is invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
axios.get(configUrl.toString()).then(({ data }) => {
|
axios
|
||||||
res.json({
|
.get(configUrl.toString())
|
||||||
issuer: data.issuer,
|
.then(({ data }) => {
|
||||||
authorization_endpoint: data.authorization_endpoint,
|
res.json({
|
||||||
token_endpoint: data.token_endpoint,
|
issuer: data.issuer,
|
||||||
userinfo_endpoint: data.userinfo_endpoint,
|
authorization_endpoint: data.authorization_endpoint,
|
||||||
end_session_endpoint: data.end_session_endpoint,
|
token_endpoint: data.token_endpoint,
|
||||||
jwks_uri: data.jwks_uri,
|
userinfo_endpoint: data.userinfo_endpoint,
|
||||||
id_token_signing_alg_values_supported: data.id_token_signing_alg_values_supported
|
end_session_endpoint: data.end_session_endpoint,
|
||||||
|
jwks_uri: data.jwks_uri,
|
||||||
|
id_token_signing_alg_values_supported: data.id_token_signing_alg_values_supported
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
Logger.error(`[Auth] Failed to get openid configuration at "${configUrl}"`, error)
|
||||||
|
res.status(error.statusCode || 400).send(`${error.code || 'UNKNOWN'}: Failed to get openid configuration`)
|
||||||
})
|
})
|
||||||
}).catch((error) => {
|
|
||||||
Logger.error(`[Auth] Failed to get openid configuration at "${configUrl}"`, error)
|
|
||||||
res.status(error.statusCode || 400).send(`${error.code || 'UNKNOWN'}: Failed to get openid configuration`)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Logout route
|
// Logout route
|
||||||
@ -683,7 +703,7 @@ class Auth {
|
|||||||
let postLogoutRedirectUri = null
|
let postLogoutRedirectUri = null
|
||||||
|
|
||||||
if (authMethod === 'openid') {
|
if (authMethod === 'openid') {
|
||||||
const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http'
|
const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http'
|
||||||
const host = req.get('host')
|
const host = req.get('host')
|
||||||
// TODO: ABS does currently not support subfolders for installation
|
// TODO: ABS does currently not support subfolders for installation
|
||||||
// If we want to support it we need to include a config for the serverurl
|
// If we want to support it we need to include a config for the serverurl
|
||||||
@ -727,7 +747,7 @@ class Auth {
|
|||||||
next()
|
next()
|
||||||
} else {
|
} else {
|
||||||
// try JWT to authenticate
|
// try JWT to authenticate
|
||||||
passport.authenticate("jwt")(req, res, next)
|
passport.authenticate('jwt')(req, res, next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -750,8 +770,7 @@ class Auth {
|
|||||||
static validateAccessToken(token) {
|
static validateAccessToken(token) {
|
||||||
try {
|
try {
|
||||||
return jwt.verify(token, global.ServerSettings.tokenSecret)
|
return jwt.verify(token, global.ServerSettings.tokenSecret)
|
||||||
}
|
} catch (err) {
|
||||||
catch (err) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -760,7 +779,8 @@ class Auth {
|
|||||||
* Generate a token which is used to encrpt/protect the jwts.
|
* Generate a token which is used to encrpt/protect the jwts.
|
||||||
*/
|
*/
|
||||||
async initTokenSecret() {
|
async initTokenSecret() {
|
||||||
if (process.env.TOKEN_SECRET) { // User can supply their own token secret
|
if (process.env.TOKEN_SECRET) {
|
||||||
|
// User can supply their own token secret
|
||||||
Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET
|
Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET
|
||||||
} else {
|
} else {
|
||||||
Database.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64')
|
Database.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64')
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const uuidv4 = require("uuid").v4
|
const uuidv4 = require('uuid').v4
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
const Database = require('../Database')
|
const Database = require('../Database')
|
||||||
@ -7,18 +7,32 @@ const User = require('../objects/user/User')
|
|||||||
|
|
||||||
const { toNumber } = require('../utils/index')
|
const { toNumber } = require('../utils/index')
|
||||||
|
|
||||||
class UserController {
|
/**
|
||||||
constructor() { }
|
* @typedef UserControllerRequestProps
|
||||||
|
* @property {import('../objects/user/User')} user - User that made the request
|
||||||
|
* @property {import('../objects/user/User')} [reqUser] - User for req param id
|
||||||
|
*
|
||||||
|
* @typedef {import('express').Request & UserControllerRequestProps} UserControllerRequest
|
||||||
|
* @typedef {import('express').Response} UserControllerResponse
|
||||||
|
*/
|
||||||
|
|
||||||
|
class UserController {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {UserControllerRequest} req
|
||||||
|
* @param {UserControllerResponse} res
|
||||||
|
*/
|
||||||
async findAll(req, res) {
|
async findAll(req, res) {
|
||||||
if (!req.user.isAdminOrUp) return res.sendStatus(403)
|
if (!req.user.isAdminOrUp) return res.sendStatus(403)
|
||||||
const hideRootToken = !req.user.isRoot
|
const hideRootToken = !req.user.isRoot
|
||||||
|
|
||||||
const includes = (req.query.include || '').split(',').map(i => i.trim())
|
const includes = (req.query.include || '').split(',').map((i) => i.trim())
|
||||||
|
|
||||||
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks
|
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks
|
||||||
const allUsers = await Database.userModel.getOldUsers()
|
const allUsers = await Database.userModel.getOldUsers()
|
||||||
const users = allUsers.map(u => u.toJSONForBrowser(hideRootToken, true))
|
const users = allUsers.map((u) => u.toJSONForBrowser(hideRootToken, true))
|
||||||
|
|
||||||
if (includes.includes('latestSession')) {
|
if (includes.includes('latestSession')) {
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
@ -37,8 +51,8 @@ class UserController {
|
|||||||
* Get a single user toJSONForBrowser
|
* Get a single user toJSONForBrowser
|
||||||
* Media progress items include: `displayTitle`, `displaySubtitle` (for podcasts), `coverPath` and `mediaUpdatedAt`
|
* Media progress items include: `displayTitle`, `displaySubtitle` (for podcasts), `coverPath` and `mediaUpdatedAt`
|
||||||
*
|
*
|
||||||
* @param {import("express").Request} req
|
* @param {UserControllerRequest} req
|
||||||
* @param {import("express").Response} res
|
* @param {UserControllerResponse} res
|
||||||
*/
|
*/
|
||||||
async findOne(req, res) {
|
async findOne(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
@ -67,7 +81,7 @@ class UserController {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const oldMediaProgresses = mediaProgresses.map(mp => {
|
const oldMediaProgresses = mediaProgresses.map((mp) => {
|
||||||
const oldMediaProgress = mp.getOldMediaProgress()
|
const oldMediaProgress = mp.getOldMediaProgress()
|
||||||
oldMediaProgress.displayTitle = mp.mediaItem?.title
|
oldMediaProgress.displayTitle = mp.mediaItem?.title
|
||||||
if (mp.mediaItem?.podcast) {
|
if (mp.mediaItem?.podcast) {
|
||||||
@ -119,8 +133,8 @@ class UserController {
|
|||||||
* PATCH: /api/users/:id
|
* PATCH: /api/users/:id
|
||||||
* Update user
|
* Update user
|
||||||
*
|
*
|
||||||
* @param {import('express').Request} req
|
* @param {UserControllerRequest} req
|
||||||
* @param {import('express').Response} res
|
* @param {UserControllerResponse} res
|
||||||
*/
|
*/
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
const user = req.reqUser
|
const user = req.reqUser
|
||||||
@ -197,8 +211,8 @@ class UserController {
|
|||||||
/**
|
/**
|
||||||
* PATCH: /api/users/:id/openid-unlink
|
* PATCH: /api/users/:id/openid-unlink
|
||||||
*
|
*
|
||||||
* @param {import('express').Request} req
|
* @param {UserControllerRequest} req
|
||||||
* @param {import('express').Response} res
|
* @param {UserControllerResponse} res
|
||||||
*/
|
*/
|
||||||
async unlinkFromOpenID(req, res) {
|
async unlinkFromOpenID(req, res) {
|
||||||
Logger.debug(`[UserController] Unlinking user "${req.reqUser.username}" from OpenID with sub "${req.reqUser.authOpenIDSub}"`)
|
Logger.debug(`[UserController] Unlinking user "${req.reqUser.username}" from OpenID with sub "${req.reqUser.authOpenIDSub}"`)
|
||||||
|
@ -19,6 +19,8 @@ class User extends Model {
|
|||||||
this.pash
|
this.pash
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
this.type
|
this.type
|
||||||
|
/** @type {string} */
|
||||||
|
this.token
|
||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
this.isActive
|
this.isActive
|
||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
@ -35,6 +37,8 @@ class User extends Model {
|
|||||||
this.createdAt
|
this.createdAt
|
||||||
/** @type {Date} */
|
/** @type {Date} */
|
||||||
this.updatedAt
|
this.updatedAt
|
||||||
|
/** @type {import('./MediaProgress')[]?} - Only included when extended */
|
||||||
|
this.mediaProgresses
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user