mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-22 00:07:52 +01:00
Matching user by openid sub, email or username based on server settings. Auto register user. Persist sub on User records
This commit is contained in:
parent
e140897313
commit
ee75d672e6
@ -82,14 +82,51 @@ class Auth {
|
||||
scope: 'openid profile email'
|
||||
}
|
||||
}, async (tokenset, userinfo, done) => {
|
||||
// TODO: Here is where to lookup the Abs user or register a new Abs user
|
||||
Logger.debug(`[Auth] openid callback userinfo=`, userinfo)
|
||||
|
||||
let user = null
|
||||
// TODO: Temporary lookup existing user by email. May be replaced by a setting to toggle this or use name
|
||||
if (userinfo.email && userinfo.email_verified) {
|
||||
user = await Database.userModel.getUserByEmail(userinfo.email)
|
||||
// TODO: If using existing user then save userinfo.sub on user
|
||||
if (!userinfo.sub) {
|
||||
Logger.error(`[Auth] openid callback invalid userinfo, no sub`)
|
||||
return done(null, null)
|
||||
}
|
||||
|
||||
// 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: Show some error log?
|
||||
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: Show some error log?
|
||||
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) {
|
||||
@ -368,7 +405,7 @@ class Auth {
|
||||
/**
|
||||
* Function to generate a jwt token for a given user
|
||||
*
|
||||
* @param {Object} user
|
||||
* @param {{ id:string, username:string }} user
|
||||
* @returns {string} token
|
||||
*/
|
||||
generateAccessToken(user) {
|
||||
@ -405,7 +442,7 @@ class Auth {
|
||||
const users = await Database.userModel.getOldUsers()
|
||||
if (users.length) {
|
||||
for (const user of users) {
|
||||
user.token = await this.generateAccessToken({ userId: user.id, username: user.username })
|
||||
user.token = await this.generateAccessToken(user)
|
||||
}
|
||||
await Database.updateBulkUsers(users)
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
const uuidv4 = require("uuid").v4
|
||||
const { DataTypes, Model, Op } = require('sequelize')
|
||||
const sequelize = require('sequelize')
|
||||
const Logger = require('../Logger')
|
||||
const oldUser = require('../objects/user/User')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const { DataTypes, Model } = sequelize
|
||||
|
||||
class User extends Model {
|
||||
constructor(values, options) {
|
||||
@ -46,6 +48,12 @@ class User extends Model {
|
||||
return users.map(u => this.getOldUser(u))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get old user model from new
|
||||
*
|
||||
* @param {Object} userExpanded
|
||||
* @returns {oldUser}
|
||||
*/
|
||||
static getOldUser(userExpanded) {
|
||||
const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress())
|
||||
|
||||
@ -72,15 +80,27 @@ class User extends Model {
|
||||
createdAt: userExpanded.createdAt.valueOf(),
|
||||
permissions,
|
||||
librariesAccessible,
|
||||
itemTagsSelected
|
||||
itemTagsSelected,
|
||||
authOpenIDSub: userExpanded.extraData?.authOpenIDSub || null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {oldUser} oldUser
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
static createFromOld(oldUser) {
|
||||
const user = this.getFromOld(oldUser)
|
||||
return this.create(user)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update User from old user model
|
||||
*
|
||||
* @param {oldUser} oldUser
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
static updateFromOld(oldUser) {
|
||||
const user = this.getFromOld(oldUser)
|
||||
return this.update(user, {
|
||||
@ -93,7 +113,21 @@ class User extends Model {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get new User model from old
|
||||
*
|
||||
* @param {oldUser} oldUser
|
||||
* @returns {Object}
|
||||
*/
|
||||
static getFromOld(oldUser) {
|
||||
const extraData = {
|
||||
seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [],
|
||||
oldUserId: oldUser.oldUserId
|
||||
}
|
||||
if (oldUser.authOpenIDSub) {
|
||||
extraData.authOpenIDSub = oldUser.authOpenIDSub
|
||||
}
|
||||
|
||||
return {
|
||||
id: oldUser.id,
|
||||
username: oldUser.username,
|
||||
@ -103,10 +137,7 @@ class User extends Model {
|
||||
token: oldUser.token || null,
|
||||
isActive: !!oldUser.isActive,
|
||||
lastSeen: oldUser.lastSeen || null,
|
||||
extraData: {
|
||||
seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [],
|
||||
oldUserId: oldUser.oldUserId
|
||||
},
|
||||
extraData,
|
||||
createdAt: oldUser.createdAt || Date.now(),
|
||||
permissions: {
|
||||
...oldUser.permissions,
|
||||
@ -130,12 +161,12 @@ class User extends Model {
|
||||
* @param {string} username
|
||||
* @param {string} pash
|
||||
* @param {Auth} auth
|
||||
* @returns {oldUser}
|
||||
* @returns {Promise<oldUser>}
|
||||
*/
|
||||
static async createRootUser(username, pash, auth) {
|
||||
const userId = uuidv4()
|
||||
|
||||
const token = await auth.generateAccessToken({ userId, username })
|
||||
const token = await auth.generateAccessToken({ id: userId, username })
|
||||
|
||||
const newRoot = new oldUser({
|
||||
id: userId,
|
||||
@ -150,6 +181,38 @@ class User extends Model {
|
||||
return newRoot
|
||||
}
|
||||
|
||||
/**
|
||||
* Create user from openid userinfo
|
||||
* @param {Object} userinfo
|
||||
* @param {Auth} auth
|
||||
* @returns {Promise<oldUser>}
|
||||
*/
|
||||
static async createUserFromOpenIdUserInfo(userinfo, auth) {
|
||||
const userId = uuidv4()
|
||||
// TODO: Ensure username is unique?
|
||||
const username = userinfo.preferred_username || userinfo.name || userinfo.sub
|
||||
const email = (userinfo.email && userinfo.email_verified) ? userinfo.email : null
|
||||
|
||||
const token = await auth.generateAccessToken({ id: userId, username })
|
||||
|
||||
const newUser = new oldUser({
|
||||
id: userId,
|
||||
type: 'user',
|
||||
username,
|
||||
email,
|
||||
pash: null,
|
||||
token,
|
||||
isActive: true,
|
||||
authOpenIDSub: userinfo.sub,
|
||||
createdAt: Date.now()
|
||||
})
|
||||
if (await this.createFromOld(newUser)) {
|
||||
SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser())
|
||||
return newUser
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by id or by the old database id
|
||||
* @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id
|
||||
@ -160,13 +223,13 @@ class User extends Model {
|
||||
if (!userId) return null
|
||||
const user = await this.findOne({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
[sequelize.Op.or]: [
|
||||
{
|
||||
id: userId
|
||||
},
|
||||
{
|
||||
extraData: {
|
||||
[Op.substring]: userId
|
||||
[sequelize.Op.substring]: userId
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -187,7 +250,7 @@ class User extends Model {
|
||||
const user = await this.findOne({
|
||||
where: {
|
||||
username: {
|
||||
[Op.like]: username
|
||||
[sequelize.Op.like]: username
|
||||
}
|
||||
},
|
||||
include: this.sequelize.models.mediaProgress
|
||||
@ -206,7 +269,7 @@ class User extends Model {
|
||||
const user = await this.findOne({
|
||||
where: {
|
||||
email: {
|
||||
[Op.like]: email
|
||||
[sequelize.Op.like]: email
|
||||
}
|
||||
},
|
||||
include: this.sequelize.models.mediaProgress
|
||||
@ -229,6 +292,21 @@ class User extends Model {
|
||||
return this.getOldUser(user)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by openid sub
|
||||
* @param {string} sub
|
||||
* @returns {Promise<oldUser|null>} returns null if not found
|
||||
*/
|
||||
static async getUserByOpenIDSub(sub) {
|
||||
if (!sub) return null
|
||||
const user = await this.findOne({
|
||||
where: sequelize.where(sequelize.literal(`extraData->>"authOpenIDSub"`), sub),
|
||||
include: this.sequelize.models.mediaProgress
|
||||
})
|
||||
if (!user) return null
|
||||
return this.getOldUser(user)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of user id and username
|
||||
* @returns {object[]} { id, username }
|
||||
|
@ -24,6 +24,8 @@ class User {
|
||||
this.librariesAccessible = [] // Library IDs (Empty if ALL libraries)
|
||||
this.itemTagsSelected = [] // Empty if ALL item tags accessible
|
||||
|
||||
this.authOpenIDSub = null
|
||||
|
||||
if (user) {
|
||||
this.construct(user)
|
||||
}
|
||||
@ -66,7 +68,7 @@ class User {
|
||||
getDefaultUserPermissions() {
|
||||
return {
|
||||
download: true,
|
||||
update: true,
|
||||
update: this.type === 'root' || this.type === 'admin',
|
||||
delete: this.type === 'root',
|
||||
upload: this.type === 'root' || this.type === 'admin',
|
||||
accessAllLibraries: true,
|
||||
@ -93,7 +95,8 @@ class User {
|
||||
createdAt: this.createdAt,
|
||||
permissions: this.permissions,
|
||||
librariesAccessible: [...this.librariesAccessible],
|
||||
itemTagsSelected: [...this.itemTagsSelected]
|
||||
itemTagsSelected: [...this.itemTagsSelected],
|
||||
authOpenIDSub: this.authOpenIDSub
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,6 +189,8 @@ class User {
|
||||
|
||||
this.librariesAccessible = [...(user.librariesAccessible || [])]
|
||||
this.itemTagsSelected = [...(user.itemTagsSelected || [])]
|
||||
|
||||
this.authOpenIDSub = user.authOpenIDSub || null
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
|
Loading…
Reference in New Issue
Block a user