diff options
Diffstat (limited to 'server/lib/auth/external-auth.ts')
-rw-r--r-- | server/lib/auth/external-auth.ts | 231 |
1 files changed, 0 insertions, 231 deletions
diff --git a/server/lib/auth/external-auth.ts b/server/lib/auth/external-auth.ts deleted file mode 100644 index bc5b74257..000000000 --- a/server/lib/auth/external-auth.ts +++ /dev/null | |||
@@ -1,231 +0,0 @@ | |||
1 | |||
2 | import { | ||
3 | isUserAdminFlagsValid, | ||
4 | isUserDisplayNameValid, | ||
5 | isUserRoleValid, | ||
6 | isUserUsernameValid, | ||
7 | isUserVideoQuotaDailyValid, | ||
8 | isUserVideoQuotaValid | ||
9 | } from '@server/helpers/custom-validators/users' | ||
10 | import { logger } from '@server/helpers/logger' | ||
11 | import { generateRandomString } from '@server/helpers/utils' | ||
12 | import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' | ||
13 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | ||
14 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | ||
15 | import { MUser } from '@server/types/models' | ||
16 | import { | ||
17 | RegisterServerAuthenticatedResult, | ||
18 | RegisterServerAuthPassOptions, | ||
19 | RegisterServerExternalAuthenticatedResult | ||
20 | } from '@server/types/plugins/register-server-auth.model' | ||
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 } | ||
27 | |||
28 | // Token is the key, expiration date is the value | ||
29 | const authBypassTokens = new Map<string, { | ||
30 | expires: Date | ||
31 | user: ExternalUser | ||
32 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] | ||
33 | authName: string | ||
34 | npmName: string | ||
35 | }>() | ||
36 | |||
37 | async function onExternalUserAuthenticated (options: { | ||
38 | npmName: string | ||
39 | authName: string | ||
40 | authResult: RegisterServerExternalAuthenticatedResult | ||
41 | }) { | ||
42 | const { npmName, authName, authResult } = options | ||
43 | |||
44 | if (!authResult.req || !authResult.res) { | ||
45 | logger.error('Cannot authenticate external user for auth %s of plugin %s: no req or res are provided.', authName, npmName) | ||
46 | return | ||
47 | } | ||
48 | |||
49 | const { res } = authResult | ||
50 | |||
51 | if (!isAuthResultValid(npmName, authName, authResult)) { | ||
52 | res.redirect('/login?externalAuthError=true') | ||
53 | return | ||
54 | } | ||
55 | |||
56 | logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName) | ||
57 | |||
58 | const bypassToken = await generateRandomString(32) | ||
59 | |||
60 | const expires = new Date() | ||
61 | expires.setTime(expires.getTime() + PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME) | ||
62 | |||
63 | const user = buildUserResult(authResult) | ||
64 | authBypassTokens.set(bypassToken, { | ||
65 | expires, | ||
66 | user, | ||
67 | npmName, | ||
68 | authName, | ||
69 | userUpdater: authResult.userUpdater | ||
70 | }) | ||
71 | |||
72 | // Cleanup expired tokens | ||
73 | const now = new Date() | ||
74 | for (const [ key, value ] of authBypassTokens) { | ||
75 | if (value.expires.getTime() < now.getTime()) { | ||
76 | authBypassTokens.delete(key) | ||
77 | } | ||
78 | } | ||
79 | |||
80 | res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) | ||
81 | } | ||
82 | |||
83 | async function getAuthNameFromRefreshGrant (refreshToken?: string) { | ||
84 | if (!refreshToken) return undefined | ||
85 | |||
86 | const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) | ||
87 | |||
88 | return tokenModel?.authName | ||
89 | } | ||
90 | |||
91 | async function getBypassFromPasswordGrant (username: string, password: string): Promise<BypassLogin> { | ||
92 | const plugins = PluginManager.Instance.getIdAndPassAuths() | ||
93 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] | ||
94 | |||
95 | for (const plugin of plugins) { | ||
96 | const auths = plugin.idAndPassAuths | ||
97 | |||
98 | for (const auth of auths) { | ||
99 | pluginAuths.push({ | ||
100 | npmName: plugin.npmName, | ||
101 | registerAuthOptions: auth | ||
102 | }) | ||
103 | } | ||
104 | } | ||
105 | |||
106 | pluginAuths.sort((a, b) => { | ||
107 | const aWeight = a.registerAuthOptions.getWeight() | ||
108 | const bWeight = b.registerAuthOptions.getWeight() | ||
109 | |||
110 | // DESC weight order | ||
111 | if (aWeight === bWeight) return 0 | ||
112 | if (aWeight < bWeight) return 1 | ||
113 | return -1 | ||
114 | }) | ||
115 | |||
116 | const loginOptions = { | ||
117 | id: username, | ||
118 | password | ||
119 | } | ||
120 | |||
121 | for (const pluginAuth of pluginAuths) { | ||
122 | const authOptions = pluginAuth.registerAuthOptions | ||
123 | const authName = authOptions.authName | ||
124 | const npmName = pluginAuth.npmName | ||
125 | |||
126 | logger.debug( | ||
127 | 'Using auth method %s of plugin %s to login %s with weight %d.', | ||
128 | authName, npmName, loginOptions.id, authOptions.getWeight() | ||
129 | ) | ||
130 | |||
131 | try { | ||
132 | const loginResult = await authOptions.login(loginOptions) | ||
133 | |||
134 | if (!loginResult) continue | ||
135 | if (!isAuthResultValid(pluginAuth.npmName, authOptions.authName, loginResult)) continue | ||
136 | |||
137 | logger.info( | ||
138 | 'Login success with auth method %s of plugin %s for %s.', | ||
139 | authName, npmName, loginOptions.id | ||
140 | ) | ||
141 | |||
142 | return { | ||
143 | bypass: true, | ||
144 | pluginName: pluginAuth.npmName, | ||
145 | authName: authOptions.authName, | ||
146 | user: buildUserResult(loginResult), | ||
147 | userUpdater: loginResult.userUpdater | ||
148 | } | ||
149 | } catch (err) { | ||
150 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) | ||
151 | } | ||
152 | } | ||
153 | |||
154 | return undefined | ||
155 | } | ||
156 | |||
157 | function getBypassFromExternalAuth (username: string, externalAuthToken: string): BypassLogin { | ||
158 | const obj = authBypassTokens.get(externalAuthToken) | ||
159 | if (!obj) throw new Error('Cannot authenticate user with unknown bypass token') | ||
160 | |||
161 | const { expires, user, authName, npmName } = obj | ||
162 | |||
163 | const now = new Date() | ||
164 | if (now.getTime() > expires.getTime()) { | ||
165 | throw new Error('Cannot authenticate user with an expired external auth token') | ||
166 | } | ||
167 | |||
168 | if (user.username !== username) { | ||
169 | throw new Error(`Cannot authenticate user ${user.username} with invalid username ${username}`) | ||
170 | } | ||
171 | |||
172 | logger.info( | ||
173 | 'Auth success with external auth method %s of plugin %s for %s.', | ||
174 | authName, npmName, user.email | ||
175 | ) | ||
176 | |||
177 | return { | ||
178 | bypass: true, | ||
179 | pluginName: npmName, | ||
180 | authName, | ||
181 | userUpdater: obj.userUpdater, | ||
182 | user | ||
183 | } | ||
184 | } | ||
185 | |||
186 | function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) { | ||
187 | const returnError = (field: string) => { | ||
188 | logger.error('Auth method %s of plugin %s did not provide a valid %s.', authName, npmName, field, { [field]: result[field] }) | ||
189 | return false | ||
190 | } | ||
191 | |||
192 | if (!isUserUsernameValid(result.username)) return returnError('username') | ||
193 | if (!result.email) return returnError('email') | ||
194 | |||
195 | // Following fields are optional | ||
196 | if (result.role && !isUserRoleValid(result.role)) return returnError('role') | ||
197 | if (result.displayName && !isUserDisplayNameValid(result.displayName)) return returnError('displayName') | ||
198 | if (result.adminFlags && !isUserAdminFlagsValid(result.adminFlags)) return returnError('adminFlags') | ||
199 | if (result.videoQuota && !isUserVideoQuotaValid(result.videoQuota + '')) return returnError('videoQuota') | ||
200 | if (result.videoQuotaDaily && !isUserVideoQuotaDailyValid(result.videoQuotaDaily + '')) return returnError('videoQuotaDaily') | ||
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 | |||
207 | return true | ||
208 | } | ||
209 | |||
210 | function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) { | ||
211 | return { | ||
212 | username: pluginResult.username, | ||
213 | email: pluginResult.email, | ||
214 | role: pluginResult.role ?? UserRole.USER, | ||
215 | displayName: pluginResult.displayName || pluginResult.username, | ||
216 | |||
217 | adminFlags: pluginResult.adminFlags ?? UserAdminFlag.NONE, | ||
218 | |||
219 | videoQuota: pluginResult.videoQuota, | ||
220 | videoQuotaDaily: pluginResult.videoQuotaDaily | ||
221 | } | ||
222 | } | ||
223 | |||
224 | // --------------------------------------------------------------------------- | ||
225 | |||
226 | export { | ||
227 | onExternalUserAuthenticated, | ||
228 | getBypassFromExternalAuth, | ||
229 | getAuthNameFromRefreshGrant, | ||
230 | getBypassFromPasswordGrant | ||
231 | } | ||