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:
advplyr 2023-11-08 16:14:57 -06:00
parent e140897313
commit ee75d672e6
3 changed files with 142 additions and 22 deletions

View File

@ -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)
} }

View File

@ -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 }

View File

@ -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) {