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