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'
|
scope: 'openid profile email'
|
||||||
}
|
}
|
||||||
}, async (tokenset, userinfo, done) => {
|
}, 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)
|
Logger.debug(`[Auth] openid callback userinfo=`, userinfo)
|
||||||
|
|
||||||
let user = null
|
if (!userinfo.sub) {
|
||||||
// TODO: Temporary lookup existing user by email. May be replaced by a setting to toggle this or use name
|
Logger.error(`[Auth] openid callback invalid userinfo, no sub`)
|
||||||
if (userinfo.email && userinfo.email_verified) {
|
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)
|
user = await Database.userModel.getUserByEmail(userinfo.email)
|
||||||
// TODO: If using existing user then save userinfo.sub on user
|
// 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) {
|
if (!user?.isActive) {
|
||||||
@ -368,7 +405,7 @@ class Auth {
|
|||||||
/**
|
/**
|
||||||
* Function to generate a jwt token for a given user
|
* Function to generate a jwt token for a given user
|
||||||
*
|
*
|
||||||
* @param {Object} user
|
* @param {{ id:string, username:string }} user
|
||||||
* @returns {string} token
|
* @returns {string} token
|
||||||
*/
|
*/
|
||||||
generateAccessToken(user) {
|
generateAccessToken(user) {
|
||||||
@ -405,7 +442,7 @@ class Auth {
|
|||||||
const users = await Database.userModel.getOldUsers()
|
const users = await Database.userModel.getOldUsers()
|
||||||
if (users.length) {
|
if (users.length) {
|
||||||
for (const user of users) {
|
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)
|
await Database.updateBulkUsers(users)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
const uuidv4 = require("uuid").v4
|
const uuidv4 = require("uuid").v4
|
||||||
const { DataTypes, Model, Op } = require('sequelize')
|
const sequelize = require('sequelize')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const oldUser = require('../objects/user/User')
|
const oldUser = require('../objects/user/User')
|
||||||
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const { DataTypes, Model } = sequelize
|
||||||
|
|
||||||
class User extends Model {
|
class User extends Model {
|
||||||
constructor(values, options) {
|
constructor(values, options) {
|
||||||
@ -46,6 +48,12 @@ class User extends Model {
|
|||||||
return users.map(u => this.getOldUser(u))
|
return users.map(u => this.getOldUser(u))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get old user model from new
|
||||||
|
*
|
||||||
|
* @param {Object} userExpanded
|
||||||
|
* @returns {oldUser}
|
||||||
|
*/
|
||||||
static getOldUser(userExpanded) {
|
static getOldUser(userExpanded) {
|
||||||
const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress())
|
const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress())
|
||||||
|
|
||||||
@ -72,15 +80,27 @@ class User extends Model {
|
|||||||
createdAt: userExpanded.createdAt.valueOf(),
|
createdAt: userExpanded.createdAt.valueOf(),
|
||||||
permissions,
|
permissions,
|
||||||
librariesAccessible,
|
librariesAccessible,
|
||||||
itemTagsSelected
|
itemTagsSelected,
|
||||||
|
authOpenIDSub: userExpanded.extraData?.authOpenIDSub || null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {oldUser} oldUser
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
static createFromOld(oldUser) {
|
static createFromOld(oldUser) {
|
||||||
const user = this.getFromOld(oldUser)
|
const user = this.getFromOld(oldUser)
|
||||||
return this.create(user)
|
return this.create(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update User from old user model
|
||||||
|
*
|
||||||
|
* @param {oldUser} oldUser
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
static updateFromOld(oldUser) {
|
static updateFromOld(oldUser) {
|
||||||
const user = this.getFromOld(oldUser)
|
const user = this.getFromOld(oldUser)
|
||||||
return this.update(user, {
|
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) {
|
static getFromOld(oldUser) {
|
||||||
|
const extraData = {
|
||||||
|
seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [],
|
||||||
|
oldUserId: oldUser.oldUserId
|
||||||
|
}
|
||||||
|
if (oldUser.authOpenIDSub) {
|
||||||
|
extraData.authOpenIDSub = oldUser.authOpenIDSub
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: oldUser.id,
|
id: oldUser.id,
|
||||||
username: oldUser.username,
|
username: oldUser.username,
|
||||||
@ -103,10 +137,7 @@ class User extends Model {
|
|||||||
token: oldUser.token || null,
|
token: oldUser.token || null,
|
||||||
isActive: !!oldUser.isActive,
|
isActive: !!oldUser.isActive,
|
||||||
lastSeen: oldUser.lastSeen || null,
|
lastSeen: oldUser.lastSeen || null,
|
||||||
extraData: {
|
extraData,
|
||||||
seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [],
|
|
||||||
oldUserId: oldUser.oldUserId
|
|
||||||
},
|
|
||||||
createdAt: oldUser.createdAt || Date.now(),
|
createdAt: oldUser.createdAt || Date.now(),
|
||||||
permissions: {
|
permissions: {
|
||||||
...oldUser.permissions,
|
...oldUser.permissions,
|
||||||
@ -130,12 +161,12 @@ class User extends Model {
|
|||||||
* @param {string} username
|
* @param {string} username
|
||||||
* @param {string} pash
|
* @param {string} pash
|
||||||
* @param {Auth} auth
|
* @param {Auth} auth
|
||||||
* @returns {oldUser}
|
* @returns {Promise<oldUser>}
|
||||||
*/
|
*/
|
||||||
static async createRootUser(username, pash, auth) {
|
static async createRootUser(username, pash, auth) {
|
||||||
const userId = uuidv4()
|
const userId = uuidv4()
|
||||||
|
|
||||||
const token = await auth.generateAccessToken({ userId, username })
|
const token = await auth.generateAccessToken({ id: userId, username })
|
||||||
|
|
||||||
const newRoot = new oldUser({
|
const newRoot = new oldUser({
|
||||||
id: userId,
|
id: userId,
|
||||||
@ -150,6 +181,38 @@ class User extends Model {
|
|||||||
return newRoot
|
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
|
* 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
|
* @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
|
if (!userId) return null
|
||||||
const user = await this.findOne({
|
const user = await this.findOne({
|
||||||
where: {
|
where: {
|
||||||
[Op.or]: [
|
[sequelize.Op.or]: [
|
||||||
{
|
{
|
||||||
id: userId
|
id: userId
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
extraData: {
|
extraData: {
|
||||||
[Op.substring]: userId
|
[sequelize.Op.substring]: userId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -187,7 +250,7 @@ class User extends Model {
|
|||||||
const user = await this.findOne({
|
const user = await this.findOne({
|
||||||
where: {
|
where: {
|
||||||
username: {
|
username: {
|
||||||
[Op.like]: username
|
[sequelize.Op.like]: username
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
include: this.sequelize.models.mediaProgress
|
include: this.sequelize.models.mediaProgress
|
||||||
@ -206,7 +269,7 @@ class User extends Model {
|
|||||||
const user = await this.findOne({
|
const user = await this.findOne({
|
||||||
where: {
|
where: {
|
||||||
email: {
|
email: {
|
||||||
[Op.like]: email
|
[sequelize.Op.like]: email
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
include: this.sequelize.models.mediaProgress
|
include: this.sequelize.models.mediaProgress
|
||||||
@ -229,6 +292,21 @@ class User extends Model {
|
|||||||
return this.getOldUser(user)
|
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
|
* Get array of user id and username
|
||||||
* @returns {object[]} { id, username }
|
* @returns {object[]} { id, username }
|
||||||
|
@ -24,6 +24,8 @@ class User {
|
|||||||
this.librariesAccessible = [] // Library IDs (Empty if ALL libraries)
|
this.librariesAccessible = [] // Library IDs (Empty if ALL libraries)
|
||||||
this.itemTagsSelected = [] // Empty if ALL item tags accessible
|
this.itemTagsSelected = [] // Empty if ALL item tags accessible
|
||||||
|
|
||||||
|
this.authOpenIDSub = null
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
this.construct(user)
|
this.construct(user)
|
||||||
}
|
}
|
||||||
@ -66,7 +68,7 @@ class User {
|
|||||||
getDefaultUserPermissions() {
|
getDefaultUserPermissions() {
|
||||||
return {
|
return {
|
||||||
download: true,
|
download: true,
|
||||||
update: true,
|
update: this.type === 'root' || this.type === 'admin',
|
||||||
delete: this.type === 'root',
|
delete: this.type === 'root',
|
||||||
upload: this.type === 'root' || this.type === 'admin',
|
upload: this.type === 'root' || this.type === 'admin',
|
||||||
accessAllLibraries: true,
|
accessAllLibraries: true,
|
||||||
@ -93,7 +95,8 @@ class User {
|
|||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
permissions: this.permissions,
|
permissions: this.permissions,
|
||||||
librariesAccessible: [...this.librariesAccessible],
|
librariesAccessible: [...this.librariesAccessible],
|
||||||
itemTagsSelected: [...this.itemTagsSelected]
|
itemTagsSelected: [...this.itemTagsSelected],
|
||||||
|
authOpenIDSub: this.authOpenIDSub
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,6 +189,8 @@ class User {
|
|||||||
|
|
||||||
this.librariesAccessible = [...(user.librariesAccessible || [])]
|
this.librariesAccessible = [...(user.librariesAccessible || [])]
|
||||||
this.itemTagsSelected = [...(user.itemTagsSelected || [])]
|
this.itemTagsSelected = [...(user.itemTagsSelected || [])]
|
||||||
|
|
||||||
|
this.authOpenIDSub = user.authOpenIDSub || null
|
||||||
}
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
|
Loading…
Reference in New Issue
Block a user