diff options
author | Chocobozzz <me@florianbigard.com> | 2022-12-30 10:12:20 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-01-04 11:41:29 +0100 |
commit | 60b880acdfa85eab5c9ec09ba1283f82ae58ec85 (patch) | |
tree | 3c89db53ea9a00e61121d76672bd931eb6d1a84a /server/lib | |
parent | 7e0c26066a5c59af742ae56bddaff9635debe034 (diff) | |
download | PeerTube-60b880acdfa85eab5c9ec09ba1283f82ae58ec85.tar.gz PeerTube-60b880acdfa85eab5c9ec09ba1283f82ae58ec85.tar.zst PeerTube-60b880acdfa85eab5c9ec09ba1283f82ae58ec85.zip |
External auth can update user on login
Diffstat (limited to 'server/lib')
-rw-r--r-- | server/lib/auth/external-auth.ts | 18 | ||||
-rw-r--r-- | server/lib/auth/oauth-model.ts | 53 |
2 files changed, 66 insertions, 5 deletions
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 { | |||
19 | RegisterServerExternalAuthenticatedResult | 19 | RegisterServerExternalAuthenticatedResult |
20 | } from '@server/types/plugins/register-server-auth.model' | 20 | } from '@server/types/plugins/register-server-auth.model' |
21 | import { UserAdminFlag, UserRole } from '@shared/models' | 21 | import { UserAdminFlag, UserRole } from '@shared/models' |
22 | import { BypassLogin } from './oauth-model' | ||
22 | 23 | ||
23 | export type ExternalUser = | 24 | export type ExternalUser = |
24 | Pick<MUser, 'username' | 'email' | 'role' | 'adminFlags' | 'videoQuotaDaily' | 'videoQuota'> & | 25 | Pick<MUser, 'username' | 'email' | 'role' | 'adminFlags' | 'videoQuotaDaily' | 'videoQuota'> & |
@@ -28,6 +29,7 @@ export type ExternalUser = | |||
28 | const authBypassTokens = new Map<string, { | 29 | const authBypassTokens = new Map<string, { |
29 | expires: Date | 30 | expires: Date |
30 | user: ExternalUser | 31 | user: ExternalUser |
32 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] | ||
31 | authName: string | 33 | authName: string |
32 | npmName: string | 34 | npmName: string |
33 | }>() | 35 | }>() |
@@ -63,7 +65,8 @@ async function onExternalUserAuthenticated (options: { | |||
63 | expires, | 65 | expires, |
64 | user, | 66 | user, |
65 | npmName, | 67 | npmName, |
66 | authName | 68 | authName, |
69 | userUpdater: authResult.userUpdater | ||
67 | }) | 70 | }) |
68 | 71 | ||
69 | // Cleanup expired tokens | 72 | // Cleanup expired tokens |
@@ -85,7 +88,7 @@ async function getAuthNameFromRefreshGrant (refreshToken?: string) { | |||
85 | return tokenModel?.authName | 88 | return tokenModel?.authName |
86 | } | 89 | } |
87 | 90 | ||
88 | async function getBypassFromPasswordGrant (username: string, password: string) { | 91 | async function getBypassFromPasswordGrant (username: string, password: string): Promise<BypassLogin> { |
89 | const plugins = PluginManager.Instance.getIdAndPassAuths() | 92 | const plugins = PluginManager.Instance.getIdAndPassAuths() |
90 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] | 93 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] |
91 | 94 | ||
@@ -140,7 +143,8 @@ async function getBypassFromPasswordGrant (username: string, password: string) { | |||
140 | bypass: true, | 143 | bypass: true, |
141 | pluginName: pluginAuth.npmName, | 144 | pluginName: pluginAuth.npmName, |
142 | authName: authOptions.authName, | 145 | authName: authOptions.authName, |
143 | user: buildUserResult(loginResult) | 146 | user: buildUserResult(loginResult), |
147 | userUpdater: loginResult.userUpdater | ||
144 | } | 148 | } |
145 | } catch (err) { | 149 | } catch (err) { |
146 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) | 150 | 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) { | |||
150 | return undefined | 154 | return undefined |
151 | } | 155 | } |
152 | 156 | ||
153 | function getBypassFromExternalAuth (username: string, externalAuthToken: string) { | 157 | function getBypassFromExternalAuth (username: string, externalAuthToken: string): BypassLogin { |
154 | const obj = authBypassTokens.get(externalAuthToken) | 158 | const obj = authBypassTokens.get(externalAuthToken) |
155 | if (!obj) throw new Error('Cannot authenticate user with unknown bypass token') | 159 | if (!obj) throw new Error('Cannot authenticate user with unknown bypass token') |
156 | 160 | ||
@@ -174,6 +178,7 @@ function getBypassFromExternalAuth (username: string, externalAuthToken: string) | |||
174 | bypass: true, | 178 | bypass: true, |
175 | pluginName: npmName, | 179 | pluginName: npmName, |
176 | authName, | 180 | authName, |
181 | userUpdater: obj.userUpdater, | ||
177 | user | 182 | user |
178 | } | 183 | } |
179 | } | 184 | } |
@@ -194,6 +199,11 @@ function isAuthResultValid (npmName: string, authName: string, result: RegisterS | |||
194 | if (result.videoQuota && !isUserVideoQuotaValid(result.videoQuota + '')) return returnError('videoQuota') | 199 | if (result.videoQuota && !isUserVideoQuotaValid(result.videoQuota + '')) return returnError('videoQuota') |
195 | if (result.videoQuotaDaily && !isUserVideoQuotaDailyValid(result.videoQuotaDaily + '')) return returnError('videoQuotaDaily') | 200 | if (result.videoQuotaDaily && !isUserVideoQuotaDailyValid(result.videoQuotaDaily + '')) return returnError('videoQuotaDaily') |
196 | 201 | ||
202 | if (result.userUpdater && typeof result.userUpdater !== 'function') { | ||
203 | logger.error('Auth method %s of plugin %s did not provide a valid user updater function.', authName, npmName) | ||
204 | return false | ||
205 | } | ||
206 | |||
197 | return true | 207 | return true |
198 | } | 208 | } |
199 | 209 | ||
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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { AccessDeniedError } from '@node-oauth/oauth2-server' | 2 | import { AccessDeniedError } from '@node-oauth/oauth2-server' |
3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
4 | import { AccountModel } from '@server/models/account/account' | ||
5 | import { AuthenticatedResultUpdaterFieldName, RegisterServerAuthenticatedResult } from '@server/types' | ||
4 | import { MOAuthClient } from '@server/types/models' | 6 | import { MOAuthClient } from '@server/types/models' |
5 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 7 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
6 | import { MUser } from '@server/types/models/user/user' | 8 | import { MUser, MUserDefault } from '@server/types/models/user/user' |
7 | import { pick } from '@shared/core-utils' | 9 | import { pick } from '@shared/core-utils' |
10 | import { AttributesOnly } from '@shared/typescript-utils' | ||
8 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
9 | import { CONFIG } from '../../initializers/config' | 12 | import { CONFIG } from '../../initializers/config' |
10 | import { OAuthClientModel } from '../../models/oauth/oauth-client' | 13 | import { OAuthClientModel } from '../../models/oauth/oauth-client' |
@@ -27,6 +30,7 @@ export type BypassLogin = { | |||
27 | pluginName: string | 30 | pluginName: string |
28 | authName?: string | 31 | authName?: string |
29 | user: ExternalUser | 32 | user: ExternalUser |
33 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] | ||
30 | } | 34 | } |
31 | 35 | ||
32 | async function getAccessToken (bearerToken: string) { | 36 | async function getAccessToken (bearerToken: string) { |
@@ -84,7 +88,9 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin | |||
84 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) | 88 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) |
85 | 89 | ||
86 | let user = await UserModel.loadByEmail(bypassLogin.user.email) | 90 | let user = await UserModel.loadByEmail(bypassLogin.user.email) |
91 | |||
87 | if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) | 92 | if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) |
93 | else user = await updateUserFromExternal(user, bypassLogin.user, bypassLogin.userUpdater) | ||
88 | 94 | ||
89 | // Cannot create a user | 95 | // Cannot create a user |
90 | if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') | 96 | 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 | |||
234 | return user | 240 | return user |
235 | } | 241 | } |
236 | 242 | ||
243 | async function updateUserFromExternal ( | ||
244 | user: MUserDefault, | ||
245 | userOptions: ExternalUser, | ||
246 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] | ||
247 | ) { | ||
248 | if (!userUpdater) return user | ||
249 | |||
250 | { | ||
251 | type UserAttributeKeys = keyof AttributesOnly<UserModel> | ||
252 | const mappingKeys: { [ id in UserAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = { | ||
253 | role: 'role', | ||
254 | adminFlags: 'adminFlags', | ||
255 | videoQuota: 'videoQuota', | ||
256 | videoQuotaDaily: 'videoQuotaDaily' | ||
257 | } | ||
258 | |||
259 | for (const modelKey of Object.keys(mappingKeys)) { | ||
260 | const pluginOptionKey = mappingKeys[modelKey] | ||
261 | |||
262 | const newValue = userUpdater({ fieldName: pluginOptionKey, currentValue: user[modelKey], newValue: userOptions[pluginOptionKey] }) | ||
263 | user.set(modelKey, newValue) | ||
264 | } | ||
265 | } | ||
266 | |||
267 | { | ||
268 | type AccountAttributeKeys = keyof Partial<AttributesOnly<AccountModel>> | ||
269 | const mappingKeys: { [ id in AccountAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = { | ||
270 | name: 'displayName' | ||
271 | } | ||
272 | |||
273 | for (const modelKey of Object.keys(mappingKeys)) { | ||
274 | const optionKey = mappingKeys[modelKey] | ||
275 | |||
276 | const newValue = userUpdater({ fieldName: optionKey, currentValue: user.Account[modelKey], newValue: userOptions[optionKey] }) | ||
277 | user.Account.set(modelKey, newValue) | ||
278 | } | ||
279 | } | ||
280 | |||
281 | logger.debug('Updated user %s with plugin userUpdated function.', user.email, { user, userOptions }) | ||
282 | |||
283 | user.Account = await user.Account.save() | ||
284 | |||
285 | return user.save() | ||
286 | } | ||
287 | |||
237 | function checkUserValidityOrThrow (user: MUser) { | 288 | function checkUserValidityOrThrow (user: MUser) { |
238 | if (user.blocked) throw new AccessDeniedError('User is blocked.') | 289 | if (user.blocked) throw new AccessDeniedError('User is blocked.') |
239 | } | 290 | } |