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'
}
}, 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)
}

View File

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

View File

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