From 60b880acdfa85eab5c9ec09ba1283f82ae58ec85 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 30 Dec 2022 10:12:20 +0100 Subject: External auth can update user on login --- server/lib/auth/external-auth.ts | 18 +++++++++++--- server/lib/auth/oauth-model.ts | 53 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 5 deletions(-) (limited to 'server/lib') diff --git a/server/lib/auth/external-auth.ts b/server/lib/auth/external-auth.ts index 155ec03d8..bc5b74257 100644 --- a/server/lib/auth/external-auth.ts +++ b/server/lib/auth/external-auth.ts @@ -19,6 +19,7 @@ import { RegisterServerExternalAuthenticatedResult } from '@server/types/plugins/register-server-auth.model' import { UserAdminFlag, UserRole } from '@shared/models' +import { BypassLogin } from './oauth-model' export type ExternalUser = Pick & @@ -28,6 +29,7 @@ export type ExternalUser = const authBypassTokens = new Map() @@ -63,7 +65,8 @@ async function onExternalUserAuthenticated (options: { expires, user, npmName, - authName + authName, + userUpdater: authResult.userUpdater }) // Cleanup expired tokens @@ -85,7 +88,7 @@ async function getAuthNameFromRefreshGrant (refreshToken?: string) { return tokenModel?.authName } -async function getBypassFromPasswordGrant (username: string, password: string) { +async function getBypassFromPasswordGrant (username: string, password: string): Promise { const plugins = PluginManager.Instance.getIdAndPassAuths() const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] @@ -140,7 +143,8 @@ async function getBypassFromPasswordGrant (username: string, password: string) { bypass: true, pluginName: pluginAuth.npmName, authName: authOptions.authName, - user: buildUserResult(loginResult) + user: buildUserResult(loginResult), + userUpdater: loginResult.userUpdater } } catch (err) { logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) @@ -150,7 +154,7 @@ async function getBypassFromPasswordGrant (username: string, password: string) { return undefined } -function getBypassFromExternalAuth (username: string, externalAuthToken: string) { +function getBypassFromExternalAuth (username: string, externalAuthToken: string): BypassLogin { const obj = authBypassTokens.get(externalAuthToken) if (!obj) throw new Error('Cannot authenticate user with unknown bypass token') @@ -174,6 +178,7 @@ function getBypassFromExternalAuth (username: string, externalAuthToken: string) bypass: true, pluginName: npmName, authName, + userUpdater: obj.userUpdater, user } } @@ -194,6 +199,11 @@ function isAuthResultValid (npmName: string, authName: string, result: RegisterS if (result.videoQuota && !isUserVideoQuotaValid(result.videoQuota + '')) return returnError('videoQuota') if (result.videoQuotaDaily && !isUserVideoQuotaDailyValid(result.videoQuotaDaily + '')) return returnError('videoQuotaDaily') + if (result.userUpdater && typeof result.userUpdater !== 'function') { + logger.error('Auth method %s of plugin %s did not provide a valid user updater function.', authName, npmName) + return false + } + return true } diff --git a/server/lib/auth/oauth-model.ts b/server/lib/auth/oauth-model.ts index 603cc0f5f..43909284f 100644 --- a/server/lib/auth/oauth-model.ts +++ b/server/lib/auth/oauth-model.ts @@ -1,10 +1,13 @@ import express from 'express' import { AccessDeniedError } from '@node-oauth/oauth2-server' import { PluginManager } from '@server/lib/plugins/plugin-manager' +import { AccountModel } from '@server/models/account/account' +import { AuthenticatedResultUpdaterFieldName, RegisterServerAuthenticatedResult } from '@server/types' import { MOAuthClient } from '@server/types/models' import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' -import { MUser } from '@server/types/models/user/user' +import { MUser, MUserDefault } from '@server/types/models/user/user' import { pick } from '@shared/core-utils' +import { AttributesOnly } from '@shared/typescript-utils' import { logger } from '../../helpers/logger' import { CONFIG } from '../../initializers/config' import { OAuthClientModel } from '../../models/oauth/oauth-client' @@ -27,6 +30,7 @@ export type BypassLogin = { pluginName: string authName?: string user: ExternalUser + userUpdater: RegisterServerAuthenticatedResult['userUpdater'] } async function getAccessToken (bearerToken: string) { @@ -84,7 +88,9 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) let user = await UserModel.loadByEmail(bypassLogin.user.email) + if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) + else user = await updateUserFromExternal(user, bypassLogin.user, bypassLogin.userUpdater) // Cannot create a user if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') @@ -234,6 +240,51 @@ async function createUserFromExternal (pluginAuth: string, userOptions: External return user } +async function updateUserFromExternal ( + user: MUserDefault, + userOptions: ExternalUser, + userUpdater: RegisterServerAuthenticatedResult['userUpdater'] +) { + if (!userUpdater) return user + + { + type UserAttributeKeys = keyof AttributesOnly + const mappingKeys: { [ id in UserAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = { + role: 'role', + adminFlags: 'adminFlags', + videoQuota: 'videoQuota', + videoQuotaDaily: 'videoQuotaDaily' + } + + for (const modelKey of Object.keys(mappingKeys)) { + const pluginOptionKey = mappingKeys[modelKey] + + const newValue = userUpdater({ fieldName: pluginOptionKey, currentValue: user[modelKey], newValue: userOptions[pluginOptionKey] }) + user.set(modelKey, newValue) + } + } + + { + type AccountAttributeKeys = keyof Partial> + const mappingKeys: { [ id in AccountAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = { + name: 'displayName' + } + + for (const modelKey of Object.keys(mappingKeys)) { + const optionKey = mappingKeys[modelKey] + + const newValue = userUpdater({ fieldName: optionKey, currentValue: user.Account[modelKey], newValue: userOptions[optionKey] }) + user.Account.set(modelKey, newValue) + } + } + + logger.debug('Updated user %s with plugin userUpdated function.', user.email, { user, userOptions }) + + user.Account = await user.Account.save() + + return user.save() +} + function checkUserValidityOrThrow (user: MUser) { if (user.blocked) throw new AccessDeniedError('User is blocked.') } -- cgit v1.2.3