diff options
Diffstat (limited to 'server/lib/auth')
-rw-r--r-- | server/lib/auth/external-auth.ts | 72 | ||||
-rw-r--r-- | server/lib/auth/oauth-model.ts | 75 | ||||
-rw-r--r-- | server/lib/auth/oauth.ts | 14 | ||||
-rw-r--r-- | server/lib/auth/tokens-cache.ts | 8 |
4 files changed, 112 insertions, 57 deletions
diff --git a/server/lib/auth/external-auth.ts b/server/lib/auth/external-auth.ts index 053112801..bc5b74257 100644 --- a/server/lib/auth/external-auth.ts +++ b/server/lib/auth/external-auth.ts | |||
@@ -1,26 +1,35 @@ | |||
1 | 1 | ||
2 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' | 2 | import { |
3 | isUserAdminFlagsValid, | ||
4 | isUserDisplayNameValid, | ||
5 | isUserRoleValid, | ||
6 | isUserUsernameValid, | ||
7 | isUserVideoQuotaDailyValid, | ||
8 | isUserVideoQuotaValid | ||
9 | } from '@server/helpers/custom-validators/users' | ||
3 | import { logger } from '@server/helpers/logger' | 10 | import { logger } from '@server/helpers/logger' |
4 | import { generateRandomString } from '@server/helpers/utils' | 11 | import { generateRandomString } from '@server/helpers/utils' |
5 | import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' | 12 | import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' |
6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 13 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | 14 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' |
15 | import { MUser } from '@server/types/models' | ||
8 | import { | 16 | import { |
9 | RegisterServerAuthenticatedResult, | 17 | RegisterServerAuthenticatedResult, |
10 | RegisterServerAuthPassOptions, | 18 | RegisterServerAuthPassOptions, |
11 | RegisterServerExternalAuthenticatedResult | 19 | RegisterServerExternalAuthenticatedResult |
12 | } from '@server/types/plugins/register-server-auth.model' | 20 | } from '@server/types/plugins/register-server-auth.model' |
13 | import { UserRole } from '@shared/models' | 21 | import { UserAdminFlag, UserRole } from '@shared/models' |
22 | import { BypassLogin } from './oauth-model' | ||
23 | |||
24 | export type ExternalUser = | ||
25 | Pick<MUser, 'username' | 'email' | 'role' | 'adminFlags' | 'videoQuotaDaily' | 'videoQuota'> & | ||
26 | { displayName: string } | ||
14 | 27 | ||
15 | // Token is the key, expiration date is the value | 28 | // Token is the key, expiration date is the value |
16 | const authBypassTokens = new Map<string, { | 29 | const authBypassTokens = new Map<string, { |
17 | expires: Date | 30 | expires: Date |
18 | user: { | 31 | user: ExternalUser |
19 | username: string | 32 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] |
20 | email: string | ||
21 | displayName: string | ||
22 | role: UserRole | ||
23 | } | ||
24 | authName: string | 33 | authName: string |
25 | npmName: string | 34 | npmName: string |
26 | }>() | 35 | }>() |
@@ -56,7 +65,8 @@ async function onExternalUserAuthenticated (options: { | |||
56 | expires, | 65 | expires, |
57 | user, | 66 | user, |
58 | npmName, | 67 | npmName, |
59 | authName | 68 | authName, |
69 | userUpdater: authResult.userUpdater | ||
60 | }) | 70 | }) |
61 | 71 | ||
62 | // Cleanup expired tokens | 72 | // Cleanup expired tokens |
@@ -78,7 +88,7 @@ async function getAuthNameFromRefreshGrant (refreshToken?: string) { | |||
78 | return tokenModel?.authName | 88 | return tokenModel?.authName |
79 | } | 89 | } |
80 | 90 | ||
81 | async function getBypassFromPasswordGrant (username: string, password: string) { | 91 | async function getBypassFromPasswordGrant (username: string, password: string): Promise<BypassLogin> { |
82 | const plugins = PluginManager.Instance.getIdAndPassAuths() | 92 | const plugins = PluginManager.Instance.getIdAndPassAuths() |
83 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] | 93 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] |
84 | 94 | ||
@@ -133,7 +143,8 @@ async function getBypassFromPasswordGrant (username: string, password: string) { | |||
133 | bypass: true, | 143 | bypass: true, |
134 | pluginName: pluginAuth.npmName, | 144 | pluginName: pluginAuth.npmName, |
135 | authName: authOptions.authName, | 145 | authName: authOptions.authName, |
136 | user: buildUserResult(loginResult) | 146 | user: buildUserResult(loginResult), |
147 | userUpdater: loginResult.userUpdater | ||
137 | } | 148 | } |
138 | } catch (err) { | 149 | } catch (err) { |
139 | 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 }) |
@@ -143,7 +154,7 @@ async function getBypassFromPasswordGrant (username: string, password: string) { | |||
143 | return undefined | 154 | return undefined |
144 | } | 155 | } |
145 | 156 | ||
146 | function getBypassFromExternalAuth (username: string, externalAuthToken: string) { | 157 | function getBypassFromExternalAuth (username: string, externalAuthToken: string): BypassLogin { |
147 | const obj = authBypassTokens.get(externalAuthToken) | 158 | const obj = authBypassTokens.get(externalAuthToken) |
148 | 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') |
149 | 160 | ||
@@ -167,33 +178,29 @@ function getBypassFromExternalAuth (username: string, externalAuthToken: string) | |||
167 | bypass: true, | 178 | bypass: true, |
168 | pluginName: npmName, | 179 | pluginName: npmName, |
169 | authName, | 180 | authName, |
181 | userUpdater: obj.userUpdater, | ||
170 | user | 182 | user |
171 | } | 183 | } |
172 | } | 184 | } |
173 | 185 | ||
174 | function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) { | 186 | function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) { |
175 | if (!isUserUsernameValid(result.username)) { | 187 | const returnError = (field: string) => { |
176 | logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { username: result.username }) | 188 | logger.error('Auth method %s of plugin %s did not provide a valid %s.', authName, npmName, field, { [field]: result[field] }) |
177 | return false | 189 | return false |
178 | } | 190 | } |
179 | 191 | ||
180 | if (!result.email) { | 192 | if (!isUserUsernameValid(result.username)) return returnError('username') |
181 | logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { email: result.email }) | 193 | if (!result.email) return returnError('email') |
182 | return false | ||
183 | } | ||
184 | 194 | ||
185 | // role is optional | 195 | // Following fields are optional |
186 | if (result.role && !isUserRoleValid(result.role)) { | 196 | if (result.role && !isUserRoleValid(result.role)) return returnError('role') |
187 | logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { role: result.role }) | 197 | if (result.displayName && !isUserDisplayNameValid(result.displayName)) return returnError('displayName') |
188 | return false | 198 | if (result.adminFlags && !isUserAdminFlagsValid(result.adminFlags)) return returnError('adminFlags') |
189 | } | 199 | if (result.videoQuota && !isUserVideoQuotaValid(result.videoQuota + '')) return returnError('videoQuota') |
200 | if (result.videoQuotaDaily && !isUserVideoQuotaDailyValid(result.videoQuotaDaily + '')) return returnError('videoQuotaDaily') | ||
190 | 201 | ||
191 | // display name is optional | 202 | if (result.userUpdater && typeof result.userUpdater !== 'function') { |
192 | if (result.displayName && !isUserDisplayNameValid(result.displayName)) { | 203 | logger.error('Auth method %s of plugin %s did not provide a valid user updater function.', authName, npmName) |
193 | logger.error( | ||
194 | 'Auth method %s of plugin %s did not provide a valid display name.', | ||
195 | authName, npmName, { displayName: result.displayName } | ||
196 | ) | ||
197 | return false | 204 | return false |
198 | } | 205 | } |
199 | 206 | ||
@@ -205,7 +212,12 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) { | |||
205 | username: pluginResult.username, | 212 | username: pluginResult.username, |
206 | email: pluginResult.email, | 213 | email: pluginResult.email, |
207 | role: pluginResult.role ?? UserRole.USER, | 214 | role: pluginResult.role ?? UserRole.USER, |
208 | displayName: pluginResult.displayName || pluginResult.username | 215 | displayName: pluginResult.displayName || pluginResult.username, |
216 | |||
217 | adminFlags: pluginResult.adminFlags ?? UserAdminFlag.NONE, | ||
218 | |||
219 | videoQuota: pluginResult.videoQuota, | ||
220 | videoQuotaDaily: pluginResult.videoQuotaDaily | ||
209 | } | 221 | } |
210 | } | 222 | } |
211 | 223 | ||
diff --git a/server/lib/auth/oauth-model.ts b/server/lib/auth/oauth-model.ts index 322b69e3a..43909284f 100644 --- a/server/lib/auth/oauth-model.ts +++ b/server/lib/auth/oauth-model.ts | |||
@@ -1,11 +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' |
8 | import { UserRole } from '@shared/models/users/user-role' | 10 | import { AttributesOnly } from '@shared/typescript-utils' |
9 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
10 | import { CONFIG } from '../../initializers/config' | 12 | import { CONFIG } from '../../initializers/config' |
11 | import { OAuthClientModel } from '../../models/oauth/oauth-client' | 13 | import { OAuthClientModel } from '../../models/oauth/oauth-client' |
@@ -13,6 +15,7 @@ import { OAuthTokenModel } from '../../models/oauth/oauth-token' | |||
13 | import { UserModel } from '../../models/user/user' | 15 | import { UserModel } from '../../models/user/user' |
14 | import { findAvailableLocalActorName } from '../local-actor' | 16 | import { findAvailableLocalActorName } from '../local-actor' |
15 | import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user' | 17 | import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user' |
18 | import { ExternalUser } from './external-auth' | ||
16 | import { TokensCache } from './tokens-cache' | 19 | import { TokensCache } from './tokens-cache' |
17 | 20 | ||
18 | type TokenInfo = { | 21 | type TokenInfo = { |
@@ -26,12 +29,8 @@ export type BypassLogin = { | |||
26 | bypass: boolean | 29 | bypass: boolean |
27 | pluginName: string | 30 | pluginName: string |
28 | authName?: string | 31 | authName?: string |
29 | user: { | 32 | user: ExternalUser |
30 | username: string | 33 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] |
31 | email: string | ||
32 | displayName: string | ||
33 | role: UserRole | ||
34 | } | ||
35 | } | 34 | } |
36 | 35 | ||
37 | async function getAccessToken (bearerToken: string) { | 36 | async function getAccessToken (bearerToken: string) { |
@@ -89,7 +88,9 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin | |||
89 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) | 88 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) |
90 | 89 | ||
91 | let user = await UserModel.loadByEmail(bypassLogin.user.email) | 90 | let user = await UserModel.loadByEmail(bypassLogin.user.email) |
91 | |||
92 | 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) | ||
93 | 94 | ||
94 | // Cannot create a user | 95 | // Cannot create a user |
95 | 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.') |
@@ -219,16 +220,11 @@ export { | |||
219 | 220 | ||
220 | // --------------------------------------------------------------------------- | 221 | // --------------------------------------------------------------------------- |
221 | 222 | ||
222 | async function createUserFromExternal (pluginAuth: string, options: { | 223 | async function createUserFromExternal (pluginAuth: string, userOptions: ExternalUser) { |
223 | username: string | 224 | const username = await findAvailableLocalActorName(userOptions.username) |
224 | email: string | ||
225 | role: UserRole | ||
226 | displayName: string | ||
227 | }) { | ||
228 | const username = await findAvailableLocalActorName(options.username) | ||
229 | 225 | ||
230 | const userToCreate = buildUser({ | 226 | const userToCreate = buildUser({ |
231 | ...pick(options, [ 'email', 'role' ]), | 227 | ...pick(userOptions, [ 'email', 'role', 'adminFlags', 'videoQuota', 'videoQuotaDaily' ]), |
232 | 228 | ||
233 | username, | 229 | username, |
234 | emailVerified: null, | 230 | emailVerified: null, |
@@ -238,12 +234,57 @@ async function createUserFromExternal (pluginAuth: string, options: { | |||
238 | 234 | ||
239 | const { user } = await createUserAccountAndChannelAndPlaylist({ | 235 | const { user } = await createUserAccountAndChannelAndPlaylist({ |
240 | userToCreate, | 236 | userToCreate, |
241 | userDisplayName: options.displayName | 237 | userDisplayName: userOptions.displayName |
242 | }) | 238 | }) |
243 | 239 | ||
244 | return user | 240 | return user |
245 | } | 241 | } |
246 | 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 | |||
247 | function checkUserValidityOrThrow (user: MUser) { | 288 | function checkUserValidityOrThrow (user: MUser) { |
248 | if (user.blocked) throw new AccessDeniedError('User is blocked.') | 289 | if (user.blocked) throw new AccessDeniedError('User is blocked.') |
249 | } | 290 | } |
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, { | |||
10 | } from '@node-oauth/oauth2-server' | 10 | } from '@node-oauth/oauth2-server' |
11 | import { randomBytesPromise } from '@server/helpers/core-utils' | 11 | import { randomBytesPromise } from '@server/helpers/core-utils' |
12 | import { isOTPValid } from '@server/helpers/otp' | 12 | import { isOTPValid } from '@server/helpers/otp' |
13 | import { CONFIG } from '@server/initializers/config' | ||
13 | import { MOAuthClient } from '@server/types/models' | 14 | import { MOAuthClient } from '@server/types/models' |
14 | import { sha1 } from '@shared/extra-utils' | 15 | import { sha1 } from '@shared/extra-utils' |
15 | import { HttpStatusCode } from '@shared/models' | 16 | import { HttpStatusCode } from '@shared/models' |
16 | import { OAUTH_LIFETIME, OTP } from '../../initializers/constants' | 17 | import { OTP } from '../../initializers/constants' |
17 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' | 18 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' |
18 | 19 | ||
19 | class MissingTwoFactorError extends Error { | 20 | class MissingTwoFactorError extends Error { |
@@ -32,8 +33,9 @@ class InvalidTwoFactorError extends Error { | |||
32 | * | 33 | * |
33 | */ | 34 | */ |
34 | const oAuthServer = new OAuth2Server({ | 35 | const oAuthServer = new OAuth2Server({ |
35 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | 36 | // Wants seconds |
36 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | 37 | accessTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN / 1000, |
38 | refreshTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN / 1000, | ||
37 | 39 | ||
38 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications | 40 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications |
39 | model: require('./oauth-model') | 41 | model: require('./oauth-model') |
@@ -182,10 +184,10 @@ function generateRandomToken () { | |||
182 | 184 | ||
183 | function getTokenExpiresAt (type: 'access' | 'refresh') { | 185 | function getTokenExpiresAt (type: 'access' | 'refresh') { |
184 | const lifetime = type === 'access' | 186 | const lifetime = type === 'access' |
185 | ? OAUTH_LIFETIME.ACCESS_TOKEN | 187 | ? CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN |
186 | : OAUTH_LIFETIME.REFRESH_TOKEN | 188 | : CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN |
187 | 189 | ||
188 | return new Date(Date.now() + lifetime * 1000) | 190 | return new Date(Date.now() + lifetime) |
189 | } | 191 | } |
190 | 192 | ||
191 | async function buildToken () { | 193 | 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 { | |||
36 | const token = this.userHavingToken.get(userId) | 36 | const token = this.userHavingToken.get(userId) |
37 | 37 | ||
38 | if (token !== undefined) { | 38 | if (token !== undefined) { |
39 | this.accessTokenCache.del(token) | 39 | this.accessTokenCache.delete(token) |
40 | this.userHavingToken.del(userId) | 40 | this.userHavingToken.delete(userId) |
41 | } | 41 | } |
42 | } | 42 | } |
43 | 43 | ||
@@ -45,8 +45,8 @@ export class TokensCache { | |||
45 | const tokenModel = this.accessTokenCache.get(token) | 45 | const tokenModel = this.accessTokenCache.get(token) |
46 | 46 | ||
47 | if (tokenModel !== undefined) { | 47 | if (tokenModel !== undefined) { |
48 | this.userHavingToken.del(tokenModel.userId) | 48 | this.userHavingToken.delete(tokenModel.userId) |
49 | this.accessTokenCache.del(token) | 49 | this.accessTokenCache.delete(token) |
50 | } | 50 | } |
51 | } | 51 | } |
52 | } | 52 | } |