mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-03-19 00:18:34 +01:00
Auth: Simplify Code
This commit is contained in:
parent
56f1bfef50
commit
f661e0835c
277
server/Auth.js
277
server/Auth.js
@ -98,139 +98,156 @@ class Auth {
|
|||||||
scope: 'openid profile email'
|
scope: 'openid profile email'
|
||||||
}
|
}
|
||||||
}, async (tokenset, userinfo, done) => {
|
}, async (tokenset, userinfo, done) => {
|
||||||
Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2))
|
try {
|
||||||
|
Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2))
|
||||||
|
|
||||||
|
if (!userinfo.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 || !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
|
||||||
|
|
||||||
let failureMessage = 'Unauthorized'
|
return done(null, user)
|
||||||
if (!userinfo.sub) {
|
} catch (error) {
|
||||||
Logger.error(`[Auth] openid callback invalid userinfo, no sub`)
|
Logger.error(`[Auth] openid callback error: ${error?.message}\n${error?.stack}`)
|
||||||
return done(null, null, failureMessage)
|
|
||||||
|
return done(null, null, 'Unauthorized')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the claims itself are returned correctly
|
|
||||||
const groupClaimName = Database.serverSettings.authOpenIDGroupClaim;
|
|
||||||
if (groupClaimName) {
|
|
||||||
if (!userinfo[groupClaimName]) {
|
|
||||||
Logger.error(`[Auth] openid callback invalid: Group claim ${groupClaimName} configured, but not found or empty in userinfo`)
|
|
||||||
return done(null, null, failureMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupsList = userinfo[groupClaimName]
|
|
||||||
const targetRoles = ['admin', 'user', 'guest']
|
|
||||||
|
|
||||||
// Convert the list to lowercase for case-insensitive comparison
|
|
||||||
const groupsListLowercase = groupsList.map(group => group.toLowerCase())
|
|
||||||
|
|
||||||
// Check if any of the target roles exist in the groups list
|
|
||||||
const containsTargetRole = targetRoles.some(role => groupsListLowercase.includes(role.toLowerCase()))
|
|
||||||
|
|
||||||
if (!containsTargetRole) {
|
|
||||||
Logger.info(`[Auth] openid callback: Denying access because neither admin nor user or guest is included is inside the group claim. Groups found: `, groupsList)
|
|
||||||
return done(null, null, failureMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const advancedPermsClaimName = Database.serverSettings.authOpenIDAdvancedPermsClaim
|
|
||||||
if (advancedPermsClaimName && !userinfo[advancedPermsClaimName]) {
|
|
||||||
Logger.error(`[Auth] openid callback invalid: Advanced perms claim ${advancedPermsClaimName} configured, but not found or empty in userinfo`)
|
|
||||||
return done(null, null, failureMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// First check for matching user by sub
|
|
||||||
let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub)
|
|
||||||
if (!user) {
|
|
||||||
// Optionally match existing by email or username based on server setting "authOpenIDMatchExistingBy"
|
|
||||||
if (Database.serverSettings.authOpenIDMatchExistingBy === 'email' && userinfo.email && userinfo.email_verified) {
|
|
||||||
Logger.info(`[Auth] openid: User not found, checking existing with email "${userinfo.email}"`)
|
|
||||||
user = await Database.userModel.getUserByEmail(userinfo.email)
|
|
||||||
// Check that user is not already matched
|
|
||||||
if (user?.authOpenIDSub) {
|
|
||||||
Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`)
|
|
||||||
// TODO: Message isn't actually returned to the user yet. Need to override the passport authenticated callback
|
|
||||||
failureMessage = 'A matching user was found but is already matched with another user from your auth provider'
|
|
||||||
user = null
|
|
||||||
}
|
|
||||||
} else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username' && userinfo.preferred_username) {
|
|
||||||
Logger.info(`[Auth] openid: User not found, checking existing with username "${userinfo.preferred_username}"`)
|
|
||||||
user = await Database.userModel.getUserByUsername(userinfo.preferred_username)
|
|
||||||
// Check that user is not already matched
|
|
||||||
if (user?.authOpenIDSub) {
|
|
||||||
Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`)
|
|
||||||
// TODO: Message isn't actually returned to the user yet. Need to override the passport authenticated callback
|
|
||||||
failureMessage = 'A matching user was found but is already matched with another user from your auth provider'
|
|
||||||
user = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If existing user was matched and isActive then save sub to user
|
|
||||||
if (user?.isActive) {
|
|
||||||
Logger.info(`[Auth] openid: New user found matching existing user "${user.username}"`)
|
|
||||||
user.authOpenIDSub = userinfo.sub
|
|
||||||
await Database.userModel.updateFromOld(user)
|
|
||||||
} else if (user && !user.isActive) {
|
|
||||||
Logger.warn(`[Auth] openid: New user found matching existing user "${user.username}" but that user is deactivated`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optionally auto register the user
|
|
||||||
if (!user && Database.serverSettings.authOpenIDAutoRegister) {
|
|
||||||
Logger.info(`[Auth] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo)
|
|
||||||
user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user?.isActive) {
|
|
||||||
if (user && !user.isActive) {
|
|
||||||
failureMessage = 'Unauthorized'
|
|
||||||
}
|
|
||||||
// deny login
|
|
||||||
done(null, null, failureMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set user group if name of groups claim is configured
|
|
||||||
if (groupClaimName) {
|
|
||||||
const groupsList = userinfo[groupClaimName] ? userinfo[groupClaimName].map(group => group.toLowerCase()) : []
|
|
||||||
const rolesInOrderOfPriority = ['admin', 'user', 'guest']
|
|
||||||
|
|
||||||
let userType = null
|
|
||||||
|
|
||||||
for (let role of rolesInOrderOfPriority) {
|
|
||||||
if (groupsList.includes(role)) {
|
|
||||||
userType = role // This will override with the highest priority role found
|
|
||||||
break // Stop searching once the highest priority role is found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actually already checked above, but just to be sure
|
|
||||||
if (!userType) {
|
|
||||||
Logger.error(`[Auth] openid callback: Denying access because neither admin nor user or guest is included is inside the group claim. Groups found: `, groupsList)
|
|
||||||
return done(null, null, failureMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.debug(`[Auth] openid callback: Setting user ${user.username} type to ${userType}`)
|
|
||||||
user.type = userType
|
|
||||||
await Database.userModel.updateFromOld(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (advancedPermsClaimName) {
|
|
||||||
try {
|
|
||||||
Logger.debug(`[Auth] openid callback: Updating advanced perms for user ${user.username} to ${JSON.stringify(userinfo[advancedPermsClaimName])}`)
|
|
||||||
|
|
||||||
user.updatePermissionsFromExternalJSON(userinfo[advancedPermsClaimName])
|
|
||||||
await Database.userModel.updateFromOld(user)
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(`[Auth] openid callback: Error updating advanced perms for user, error: `, error)
|
|
||||||
return done(null, null, failureMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// permit login
|
|
||||||
return done(null, user)
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an existing user by OpenID subject identifier, or by email/username based on server settings,
|
||||||
|
* or creates a new user if configured to do so.
|
||||||
|
*/
|
||||||
|
async findOrCreateUser(userinfo) {
|
||||||
|
let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub)
|
||||||
|
|
||||||
|
// Matched by sub
|
||||||
|
if (user) {
|
||||||
|
Logger.debug(`[Auth] openid: User found by sub`)
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match existing user by email
|
||||||
|
if (Database.serverSettings.authOpenIDMatchExistingBy === 'email' && userinfo.email && userinfo.email_verified) {
|
||||||
|
Logger.info(`[Auth] openid: User not found, checking existing with email "${userinfo.email}"`)
|
||||||
|
user = await Database.userModel.getUserByEmail(userinfo.email)
|
||||||
|
|
||||||
|
if (user?.authOpenIDSub) {
|
||||||
|
Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`)
|
||||||
|
return null // User is linked to a different OpenID subject; do not proceed.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Match existing user by username
|
||||||
|
else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username' && userinfo.preferred_username) {
|
||||||
|
Logger.info(`[Auth] openid: User not found, checking existing with username "${userinfo.preferred_username}"`)
|
||||||
|
user = await Database.userModel.getUserByUsername(userinfo.preferred_username)
|
||||||
|
|
||||||
|
if (user?.authOpenIDSub) {
|
||||||
|
Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`)
|
||||||
|
return null // User is linked to a different OpenID subject; do not proceed.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found existing user via email or username
|
||||||
|
if (user) {
|
||||||
|
if (!user.isActive) {
|
||||||
|
Logger.warn(`[Auth] openid: User found but is not active`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
user.authOpenIDSub = userinfo.sub
|
||||||
|
await Database.userModel.updateFromOld(user)
|
||||||
|
|
||||||
|
Logger.debug(`[Auth] openid: User found by email/username`)
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no existing user was matched, auto-register if configured
|
||||||
|
if (Database.serverSettings.authOpenIDAutoRegister) {
|
||||||
|
Logger.info(`[Auth] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo)
|
||||||
|
user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo, this)
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.warn(`[Auth] openid: User not found and auto-register is disabled`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the presence and content of the group claim in userinfo.
|
||||||
|
*/
|
||||||
|
validateGroupClaim(userinfo) {
|
||||||
|
const groupClaimName = Database.serverSettings.authOpenIDGroupClaim;
|
||||||
|
if (!groupClaimName) // Allow no group claim when configured like this
|
||||||
|
return true
|
||||||
|
|
||||||
|
// If configured it must exist in userinfo
|
||||||
|
if (!userinfo[groupClaimName]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the user group based on group claim in userinfo.
|
||||||
|
*/
|
||||||
|
async setUserGroup(user, userinfo) {
|
||||||
|
const groupClaimName = Database.serverSettings.authOpenIDGroupClaim;
|
||||||
|
if (!groupClaimName) // No group claim configured, don't set anything
|
||||||
|
return
|
||||||
|
|
||||||
|
if (!userinfo[groupClaimName])
|
||||||
|
throw new Error(`Group claim ${groupClaimName} not found in userinfo`)
|
||||||
|
|
||||||
|
const groupsList = userinfo[groupClaimName].map(group => group.toLowerCase())
|
||||||
|
const rolesInOrderOfPriority = ['admin', 'user', 'guest']
|
||||||
|
|
||||||
|
let userType = rolesInOrderOfPriority.find(role => groupsList.includes(role))
|
||||||
|
if (userType) {
|
||||||
|
Logger.debug(`[Auth] openid callback: Setting user ${user.username} type to ${userType}`)
|
||||||
|
|
||||||
|
if (user.type !== userType) {
|
||||||
|
user.type = userType;
|
||||||
|
await Database.userModel.updateFromOld(user)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`No valid group found in userinfo: ${JSON.stringify(userinfo[groupClaimName], null, 2)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates user permissions based on the advanced permissions claim.
|
||||||
|
*/
|
||||||
|
async updateUserPermissions(user, userinfo) {
|
||||||
|
const absPermissionsClaim = Database.serverSettings.authOpenIDAdvancedPermsClaim
|
||||||
|
if (!absPermissionsClaim) // No advanced permissions claim configured, don't set anything
|
||||||
|
return
|
||||||
|
|
||||||
|
const absPermissions = userinfo[absPermissionsClaim]
|
||||||
|
if (!absPermissions)
|
||||||
|
throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`)
|
||||||
|
|
||||||
|
Logger.debug(`[Auth] openid callback: Updating advanced perms for user ${user.username} to ${JSON.stringify(absPermissions)}`)
|
||||||
|
user.updatePermissionsFromExternalJSON(absPermissions)
|
||||||
|
await Database.userModel.updateFromOld(user)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unuse strategy
|
* Unuse strategy
|
||||||
*
|
*
|
||||||
@ -421,7 +438,7 @@ class Auth {
|
|||||||
|
|
||||||
res.redirect(authorizationUrl)
|
res.redirect(authorizationUrl)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[Auth] Error in /auth/openid route: ${error}`)
|
Logger.error(`[Auth] Error in /auth/openid route: ${error}\n${error?.stack}`)
|
||||||
res.status(500).send('Internal Server Error')
|
res.status(500).send('Internal Server Error')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,7 +494,7 @@ class Auth {
|
|||||||
// Redirect to the overwrite URI saved in the map
|
// Redirect to the overwrite URI saved in the map
|
||||||
res.redirect(redirectUri)
|
res.redirect(redirectUri)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[Auth] Error in /auth/openid/mobile-redirect route: ${error}`)
|
Logger.error(`[Auth] Error in /auth/openid/mobile-redirect route: ${error}\n${error?.stack}`)
|
||||||
res.status(500).send('Internal Server Error')
|
res.status(500).send('Internal Server Error')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user