From b65f5367baf799b425be0bcfb9220922751bb6eb Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 29 Dec 2022 14:18:07 +0100 Subject: Add ability to customize token lifetime --- server/lib/auth/oauth.ts | 14 ++++++++------ server/lib/auth/tokens-cache.ts | 8 ++++---- 2 files changed, 12 insertions(+), 10 deletions(-) (limited to 'server/lib') diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts index bc0d4301f..2905c79a2 100644 --- a/server/lib/auth/oauth.ts +++ b/server/lib/auth/oauth.ts @@ -10,10 +10,11 @@ import OAuth2Server, { } from '@node-oauth/oauth2-server' import { randomBytesPromise } from '@server/helpers/core-utils' import { isOTPValid } from '@server/helpers/otp' +import { CONFIG } from '@server/initializers/config' import { MOAuthClient } from '@server/types/models' import { sha1 } from '@shared/extra-utils' import { HttpStatusCode } from '@shared/models' -import { OAUTH_LIFETIME, OTP } from '../../initializers/constants' +import { OTP } from '../../initializers/constants' import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' class MissingTwoFactorError extends Error { @@ -32,8 +33,9 @@ class InvalidTwoFactorError extends Error { * */ const oAuthServer = new OAuth2Server({ - accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, - refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, + // Wants seconds + accessTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN / 1000, + refreshTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN / 1000, // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications model: require('./oauth-model') @@ -182,10 +184,10 @@ function generateRandomToken () { function getTokenExpiresAt (type: 'access' | 'refresh') { const lifetime = type === 'access' - ? OAUTH_LIFETIME.ACCESS_TOKEN - : OAUTH_LIFETIME.REFRESH_TOKEN + ? CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN + : CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN - return new Date(Date.now() + lifetime * 1000) + return new Date(Date.now() + lifetime) } async function buildToken () { diff --git a/server/lib/auth/tokens-cache.ts b/server/lib/auth/tokens-cache.ts index 410708a35..43efc7d02 100644 --- a/server/lib/auth/tokens-cache.ts +++ b/server/lib/auth/tokens-cache.ts @@ -36,8 +36,8 @@ export class TokensCache { const token = this.userHavingToken.get(userId) if (token !== undefined) { - this.accessTokenCache.del(token) - this.userHavingToken.del(userId) + this.accessTokenCache.delete(token) + this.userHavingToken.delete(userId) } } @@ -45,8 +45,8 @@ export class TokensCache { const tokenModel = this.accessTokenCache.get(token) if (tokenModel !== undefined) { - this.userHavingToken.del(tokenModel.userId) - this.accessTokenCache.del(token) + this.userHavingToken.delete(tokenModel.userId) + this.accessTokenCache.delete(token) } } } -- cgit v1.2.3 From 7e0c26066a5c59af742ae56bddaff9635debe034 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 29 Dec 2022 15:31:40 +0100 Subject: External auth can set more user fields videoQuota, videoQuotaDaily, adminFlags --- server/lib/auth/external-auth.ts | 60 +++++++++++++++++++++------------------- server/lib/auth/oauth-model.ts | 22 ++++----------- 2 files changed, 37 insertions(+), 45 deletions(-) (limited to 'server/lib') diff --git a/server/lib/auth/external-auth.ts b/server/lib/auth/external-auth.ts index 053112801..155ec03d8 100644 --- a/server/lib/auth/external-auth.ts +++ b/server/lib/auth/external-auth.ts @@ -1,26 +1,33 @@ -import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' +import { + isUserAdminFlagsValid, + isUserDisplayNameValid, + isUserRoleValid, + isUserUsernameValid, + isUserVideoQuotaDailyValid, + isUserVideoQuotaValid +} from '@server/helpers/custom-validators/users' import { logger } from '@server/helpers/logger' import { generateRandomString } from '@server/helpers/utils' import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' import { PluginManager } from '@server/lib/plugins/plugin-manager' import { OAuthTokenModel } from '@server/models/oauth/oauth-token' +import { MUser } from '@server/types/models' import { RegisterServerAuthenticatedResult, RegisterServerAuthPassOptions, RegisterServerExternalAuthenticatedResult } from '@server/types/plugins/register-server-auth.model' -import { UserRole } from '@shared/models' +import { UserAdminFlag, UserRole } from '@shared/models' + +export type ExternalUser = + Pick & + { displayName: string } // Token is the key, expiration date is the value const authBypassTokens = new Map() @@ -172,30 +179,20 @@ function getBypassFromExternalAuth (username: string, externalAuthToken: string) } function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) { - if (!isUserUsernameValid(result.username)) { - logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { username: result.username }) + const returnError = (field: string) => { + logger.error('Auth method %s of plugin %s did not provide a valid %s.', authName, npmName, field, { [field]: result[field] }) return false } - if (!result.email) { - logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { email: result.email }) - return false - } + if (!isUserUsernameValid(result.username)) return returnError('username') + if (!result.email) return returnError('email') - // role is optional - if (result.role && !isUserRoleValid(result.role)) { - logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { role: result.role }) - return false - } - - // display name is optional - if (result.displayName && !isUserDisplayNameValid(result.displayName)) { - logger.error( - 'Auth method %s of plugin %s did not provide a valid display name.', - authName, npmName, { displayName: result.displayName } - ) - return false - } + // Following fields are optional + if (result.role && !isUserRoleValid(result.role)) return returnError('role') + if (result.displayName && !isUserDisplayNameValid(result.displayName)) return returnError('displayName') + if (result.adminFlags && !isUserAdminFlagsValid(result.adminFlags)) return returnError('adminFlags') + if (result.videoQuota && !isUserVideoQuotaValid(result.videoQuota + '')) return returnError('videoQuota') + if (result.videoQuotaDaily && !isUserVideoQuotaDailyValid(result.videoQuotaDaily + '')) return returnError('videoQuotaDaily') return true } @@ -205,7 +202,12 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) { username: pluginResult.username, email: pluginResult.email, role: pluginResult.role ?? UserRole.USER, - displayName: pluginResult.displayName || pluginResult.username + displayName: pluginResult.displayName || pluginResult.username, + + adminFlags: pluginResult.adminFlags ?? UserAdminFlag.NONE, + + videoQuota: pluginResult.videoQuota, + videoQuotaDaily: pluginResult.videoQuotaDaily } } diff --git a/server/lib/auth/oauth-model.ts b/server/lib/auth/oauth-model.ts index 322b69e3a..603cc0f5f 100644 --- a/server/lib/auth/oauth-model.ts +++ b/server/lib/auth/oauth-model.ts @@ -5,7 +5,6 @@ import { MOAuthClient } from '@server/types/models' import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' import { MUser } from '@server/types/models/user/user' import { pick } from '@shared/core-utils' -import { UserRole } from '@shared/models/users/user-role' import { logger } from '../../helpers/logger' import { CONFIG } from '../../initializers/config' import { OAuthClientModel } from '../../models/oauth/oauth-client' @@ -13,6 +12,7 @@ import { OAuthTokenModel } from '../../models/oauth/oauth-token' import { UserModel } from '../../models/user/user' import { findAvailableLocalActorName } from '../local-actor' import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user' +import { ExternalUser } from './external-auth' import { TokensCache } from './tokens-cache' type TokenInfo = { @@ -26,12 +26,7 @@ export type BypassLogin = { bypass: boolean pluginName: string authName?: string - user: { - username: string - email: string - displayName: string - role: UserRole - } + user: ExternalUser } async function getAccessToken (bearerToken: string) { @@ -219,16 +214,11 @@ export { // --------------------------------------------------------------------------- -async function createUserFromExternal (pluginAuth: string, options: { - username: string - email: string - role: UserRole - displayName: string -}) { - const username = await findAvailableLocalActorName(options.username) +async function createUserFromExternal (pluginAuth: string, userOptions: ExternalUser) { + const username = await findAvailableLocalActorName(userOptions.username) const userToCreate = buildUser({ - ...pick(options, [ 'email', 'role' ]), + ...pick(userOptions, [ 'email', 'role', 'adminFlags', 'videoQuota', 'videoQuotaDaily' ]), username, emailVerified: null, @@ -238,7 +228,7 @@ async function createUserFromExternal (pluginAuth: string, options: { const { user } = await createUserAccountAndChannelAndPlaylist({ userToCreate, - userDisplayName: options.displayName + userDisplayName: userOptions.displayName }) return user -- cgit v1.2.3 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