diff options
Diffstat (limited to 'server/lib/auth/oauth-model.ts')
-rw-r--r-- | server/lib/auth/oauth-model.ts | 294 |
1 files changed, 0 insertions, 294 deletions
diff --git a/server/lib/auth/oauth-model.ts b/server/lib/auth/oauth-model.ts deleted file mode 100644 index d3a5eccd5..000000000 --- a/server/lib/auth/oauth-model.ts +++ /dev/null | |||
@@ -1,294 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { AccessDeniedError } from '@node-oauth/oauth2-server' | ||
3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | ||
4 | import { AccountModel } from '@server/models/account/account' | ||
5 | import { AuthenticatedResultUpdaterFieldName, RegisterServerAuthenticatedResult } from '@server/types' | ||
6 | import { MOAuthClient } from '@server/types/models' | ||
7 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | ||
8 | import { MUser, MUserDefault } from '@server/types/models/user/user' | ||
9 | import { pick } from '@shared/core-utils' | ||
10 | import { AttributesOnly } from '@shared/typescript-utils' | ||
11 | import { logger } from '../../helpers/logger' | ||
12 | import { CONFIG } from '../../initializers/config' | ||
13 | import { OAuthClientModel } from '../../models/oauth/oauth-client' | ||
14 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' | ||
15 | import { UserModel } from '../../models/user/user' | ||
16 | import { findAvailableLocalActorName } from '../local-actor' | ||
17 | import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user' | ||
18 | import { ExternalUser } from './external-auth' | ||
19 | import { TokensCache } from './tokens-cache' | ||
20 | |||
21 | type TokenInfo = { | ||
22 | accessToken: string | ||
23 | refreshToken: string | ||
24 | accessTokenExpiresAt: Date | ||
25 | refreshTokenExpiresAt: Date | ||
26 | } | ||
27 | |||
28 | export type BypassLogin = { | ||
29 | bypass: boolean | ||
30 | pluginName: string | ||
31 | authName?: string | ||
32 | user: ExternalUser | ||
33 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] | ||
34 | } | ||
35 | |||
36 | async function getAccessToken (bearerToken: string) { | ||
37 | logger.debug('Getting access token.') | ||
38 | |||
39 | if (!bearerToken) return undefined | ||
40 | |||
41 | let tokenModel: MOAuthTokenUser | ||
42 | |||
43 | if (TokensCache.Instance.hasToken(bearerToken)) { | ||
44 | tokenModel = TokensCache.Instance.getByToken(bearerToken) | ||
45 | } else { | ||
46 | tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) | ||
47 | |||
48 | if (tokenModel) TokensCache.Instance.setToken(tokenModel) | ||
49 | } | ||
50 | |||
51 | if (!tokenModel) return undefined | ||
52 | |||
53 | if (tokenModel.User.pluginAuth) { | ||
54 | const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'access') | ||
55 | |||
56 | if (valid !== true) return undefined | ||
57 | } | ||
58 | |||
59 | return tokenModel | ||
60 | } | ||
61 | |||
62 | function getClient (clientId: string, clientSecret: string) { | ||
63 | logger.debug('Getting Client (clientId: ' + clientId + ', clientSecret: ' + clientSecret + ').') | ||
64 | |||
65 | return OAuthClientModel.getByIdAndSecret(clientId, clientSecret) | ||
66 | } | ||
67 | |||
68 | async function getRefreshToken (refreshToken: string) { | ||
69 | logger.debug('Getting RefreshToken (refreshToken: ' + refreshToken + ').') | ||
70 | |||
71 | const tokenInfo = await OAuthTokenModel.getByRefreshTokenAndPopulateClient(refreshToken) | ||
72 | if (!tokenInfo) return undefined | ||
73 | |||
74 | const tokenModel = tokenInfo.token | ||
75 | |||
76 | if (tokenModel.User.pluginAuth) { | ||
77 | const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'refresh') | ||
78 | |||
79 | if (valid !== true) return undefined | ||
80 | } | ||
81 | |||
82 | return tokenInfo | ||
83 | } | ||
84 | |||
85 | async function getUser (usernameOrEmail?: string, password?: string, bypassLogin?: BypassLogin) { | ||
86 | // Special treatment coming from a plugin | ||
87 | if (bypassLogin && bypassLogin.bypass === true) { | ||
88 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) | ||
89 | |||
90 | let user = await UserModel.loadByEmail(bypassLogin.user.email) | ||
91 | |||
92 | if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) | ||
93 | else user = await updateUserFromExternal(user, bypassLogin.user, bypassLogin.userUpdater) | ||
94 | |||
95 | // Cannot create a user | ||
96 | if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') | ||
97 | |||
98 | // If the user does not belongs to a plugin, it was created before its installation | ||
99 | // Then we just go through a regular login process | ||
100 | if (user.pluginAuth !== null) { | ||
101 | // This user does not belong to this plugin, skip it | ||
102 | if (user.pluginAuth !== bypassLogin.pluginName) { | ||
103 | logger.info( | ||
104 | 'Cannot bypass oauth login by plugin %s because %s has another plugin auth method (%s).', | ||
105 | bypassLogin.pluginName, bypassLogin.user.email, user.pluginAuth | ||
106 | ) | ||
107 | |||
108 | return null | ||
109 | } | ||
110 | |||
111 | checkUserValidityOrThrow(user) | ||
112 | |||
113 | return user | ||
114 | } | ||
115 | } | ||
116 | |||
117 | logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).') | ||
118 | |||
119 | const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail) | ||
120 | |||
121 | // If we don't find the user, or if the user belongs to a plugin | ||
122 | if (!user || user.pluginAuth !== null || !password) return null | ||
123 | |||
124 | const passwordMatch = await user.isPasswordMatch(password) | ||
125 | if (passwordMatch !== true) return null | ||
126 | |||
127 | checkUserValidityOrThrow(user) | ||
128 | |||
129 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION && user.emailVerified === false) { | ||
130 | throw new AccessDeniedError('User email is not verified.') | ||
131 | } | ||
132 | |||
133 | return user | ||
134 | } | ||
135 | |||
136 | async function revokeToken ( | ||
137 | tokenInfo: { refreshToken: string }, | ||
138 | options: { | ||
139 | req?: express.Request | ||
140 | explicitLogout?: boolean | ||
141 | } = {} | ||
142 | ): Promise<{ success: boolean, redirectUrl?: string }> { | ||
143 | const { req, explicitLogout } = options | ||
144 | |||
145 | const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) | ||
146 | |||
147 | if (token) { | ||
148 | let redirectUrl: string | ||
149 | |||
150 | if (explicitLogout === true && token.User.pluginAuth && token.authName) { | ||
151 | redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, req) | ||
152 | } | ||
153 | |||
154 | TokensCache.Instance.clearCacheByToken(token.accessToken) | ||
155 | |||
156 | token.destroy() | ||
157 | .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) | ||
158 | |||
159 | return { success: true, redirectUrl } | ||
160 | } | ||
161 | |||
162 | return { success: false } | ||
163 | } | ||
164 | |||
165 | async function saveToken ( | ||
166 | token: TokenInfo, | ||
167 | client: MOAuthClient, | ||
168 | user: MUser, | ||
169 | options: { | ||
170 | refreshTokenAuthName?: string | ||
171 | bypassLogin?: BypassLogin | ||
172 | } = {} | ||
173 | ) { | ||
174 | const { refreshTokenAuthName, bypassLogin } = options | ||
175 | let authName: string = null | ||
176 | |||
177 | if (bypassLogin?.bypass === true) { | ||
178 | authName = bypassLogin.authName | ||
179 | } else if (refreshTokenAuthName) { | ||
180 | authName = refreshTokenAuthName | ||
181 | } | ||
182 | |||
183 | logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') | ||
184 | |||
185 | const tokenToCreate = { | ||
186 | accessToken: token.accessToken, | ||
187 | accessTokenExpiresAt: token.accessTokenExpiresAt, | ||
188 | refreshToken: token.refreshToken, | ||
189 | refreshTokenExpiresAt: token.refreshTokenExpiresAt, | ||
190 | authName, | ||
191 | oAuthClientId: client.id, | ||
192 | userId: user.id | ||
193 | } | ||
194 | |||
195 | const tokenCreated = await OAuthTokenModel.create(tokenToCreate) | ||
196 | |||
197 | user.lastLoginDate = new Date() | ||
198 | await user.save() | ||
199 | |||
200 | return { | ||
201 | accessToken: tokenCreated.accessToken, | ||
202 | accessTokenExpiresAt: tokenCreated.accessTokenExpiresAt, | ||
203 | refreshToken: tokenCreated.refreshToken, | ||
204 | refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt, | ||
205 | client, | ||
206 | user, | ||
207 | accessTokenExpiresIn: buildExpiresIn(tokenCreated.accessTokenExpiresAt), | ||
208 | refreshTokenExpiresIn: buildExpiresIn(tokenCreated.refreshTokenExpiresAt) | ||
209 | } | ||
210 | } | ||
211 | |||
212 | export { | ||
213 | getAccessToken, | ||
214 | getClient, | ||
215 | getRefreshToken, | ||
216 | getUser, | ||
217 | revokeToken, | ||
218 | saveToken | ||
219 | } | ||
220 | |||
221 | // --------------------------------------------------------------------------- | ||
222 | |||
223 | async function createUserFromExternal (pluginAuth: string, userOptions: ExternalUser) { | ||
224 | const username = await findAvailableLocalActorName(userOptions.username) | ||
225 | |||
226 | const userToCreate = buildUser({ | ||
227 | ...pick(userOptions, [ 'email', 'role', 'adminFlags', 'videoQuota', 'videoQuotaDaily' ]), | ||
228 | |||
229 | username, | ||
230 | emailVerified: null, | ||
231 | password: null, | ||
232 | pluginAuth | ||
233 | }) | ||
234 | |||
235 | const { user } = await createUserAccountAndChannelAndPlaylist({ | ||
236 | userToCreate, | ||
237 | userDisplayName: userOptions.displayName | ||
238 | }) | ||
239 | |||
240 | return user | ||
241 | } | ||
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 | |||
288 | function checkUserValidityOrThrow (user: MUser) { | ||
289 | if (user.blocked) throw new AccessDeniedError('User is blocked.') | ||
290 | } | ||
291 | |||
292 | function buildExpiresIn (expiresAt: Date) { | ||
293 | return Math.floor((expiresAt.getTime() - new Date().getTime()) / 1000) | ||
294 | } | ||