]>
Commit | Line | Data |
---|---|---|
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | |
2 | ||
3 | import { expect } from 'chai' | |
4 | import { wait } from '@shared/core-utils' | |
5 | import { HttpStatusCode, UserAdminFlag, UserRole } from '@shared/models' | |
6 | import { | |
7 | cleanupTests, | |
8 | createSingleServer, | |
9 | decodeQueryString, | |
10 | PeerTubeServer, | |
11 | PluginsCommand, | |
12 | setAccessTokensToServers | |
13 | } from '@shared/server-commands' | |
14 | ||
15 | async function loginExternal (options: { | |
16 | server: PeerTubeServer | |
17 | npmName: string | |
18 | authName: string | |
19 | username: string | |
20 | query?: any | |
21 | expectedStatus?: HttpStatusCode | |
22 | expectedStatusStep2?: HttpStatusCode | |
23 | }) { | |
24 | const res = await options.server.plugins.getExternalAuth({ | |
25 | npmName: options.npmName, | |
26 | npmVersion: '0.0.1', | |
27 | authName: options.authName, | |
28 | query: options.query, | |
29 | expectedStatus: options.expectedStatus || HttpStatusCode.FOUND_302 | |
30 | }) | |
31 | ||
32 | if (res.status !== HttpStatusCode.FOUND_302) return | |
33 | ||
34 | const location = res.header.location | |
35 | const { externalAuthToken } = decodeQueryString(location) | |
36 | ||
37 | const resLogin = await options.server.login.loginUsingExternalToken({ | |
38 | username: options.username, | |
39 | externalAuthToken: externalAuthToken as string, | |
40 | expectedStatus: options.expectedStatusStep2 | |
41 | }) | |
42 | ||
43 | return resLogin.body | |
44 | } | |
45 | ||
46 | describe('Test external auth plugins', function () { | |
47 | let server: PeerTubeServer | |
48 | ||
49 | let cyanAccessToken: string | |
50 | let cyanRefreshToken: string | |
51 | ||
52 | let kefkaAccessToken: string | |
53 | let kefkaRefreshToken: string | |
54 | let kefkaId: number | |
55 | ||
56 | let externalAuthToken: string | |
57 | ||
58 | before(async function () { | |
59 | this.timeout(30000) | |
60 | ||
61 | server = await createSingleServer(1, { | |
62 | rates_limit: { | |
63 | login: { | |
64 | max: 30 | |
65 | } | |
66 | } | |
67 | }) | |
68 | ||
69 | await setAccessTokensToServers([ server ]) | |
70 | ||
71 | for (const suffix of [ 'one', 'two', 'three' ]) { | |
72 | await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-external-auth-' + suffix) }) | |
73 | } | |
74 | }) | |
75 | ||
76 | it('Should display the correct configuration', async function () { | |
77 | const config = await server.config.getConfig() | |
78 | ||
79 | const auths = config.plugin.registeredExternalAuths | |
80 | expect(auths).to.have.lengthOf(9) | |
81 | ||
82 | const auth2 = auths.find((a) => a.authName === 'external-auth-2') | |
83 | expect(auth2).to.exist | |
84 | expect(auth2.authDisplayName).to.equal('External Auth 2') | |
85 | expect(auth2.npmName).to.equal('peertube-plugin-test-external-auth-one') | |
86 | }) | |
87 | ||
88 | it('Should redirect for a Cyan login', async function () { | |
89 | const res = await server.plugins.getExternalAuth({ | |
90 | npmName: 'test-external-auth-one', | |
91 | npmVersion: '0.0.1', | |
92 | authName: 'external-auth-1', | |
93 | query: { | |
94 | username: 'cyan' | |
95 | }, | |
96 | expectedStatus: HttpStatusCode.FOUND_302 | |
97 | }) | |
98 | ||
99 | const location = res.header.location | |
100 | expect(location.startsWith('/login?')).to.be.true | |
101 | ||
102 | const searchParams = decodeQueryString(location) | |
103 | ||
104 | expect(searchParams.externalAuthToken).to.exist | |
105 | expect(searchParams.username).to.equal('cyan') | |
106 | ||
107 | externalAuthToken = searchParams.externalAuthToken as string | |
108 | }) | |
109 | ||
110 | it('Should reject auto external login with a missing or invalid token', async function () { | |
111 | const command = server.login | |
112 | ||
113 | await command.loginUsingExternalToken({ username: 'cyan', externalAuthToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | |
114 | await command.loginUsingExternalToken({ username: 'cyan', externalAuthToken: 'blabla', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | |
115 | }) | |
116 | ||
117 | it('Should reject auto external login with a missing or invalid username', async function () { | |
118 | const command = server.login | |
119 | ||
120 | await command.loginUsingExternalToken({ username: '', externalAuthToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | |
121 | await command.loginUsingExternalToken({ username: '', externalAuthToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | |
122 | }) | |
123 | ||
124 | it('Should reject auto external login with an expired token', async function () { | |
125 | this.timeout(15000) | |
126 | ||
127 | await wait(5000) | |
128 | ||
129 | await server.login.loginUsingExternalToken({ | |
130 | username: 'cyan', | |
131 | externalAuthToken, | |
132 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | |
133 | }) | |
134 | ||
135 | await server.servers.waitUntilLog('expired external auth token', 4) | |
136 | }) | |
137 | ||
138 | it('Should auto login Cyan, create the user and use the token', async function () { | |
139 | { | |
140 | const res = await loginExternal({ | |
141 | server, | |
142 | npmName: 'test-external-auth-one', | |
143 | authName: 'external-auth-1', | |
144 | query: { | |
145 | username: 'cyan' | |
146 | }, | |
147 | username: 'cyan' | |
148 | }) | |
149 | ||
150 | cyanAccessToken = res.access_token | |
151 | cyanRefreshToken = res.refresh_token | |
152 | } | |
153 | ||
154 | { | |
155 | const body = await server.users.getMyInfo({ token: cyanAccessToken }) | |
156 | expect(body.username).to.equal('cyan') | |
157 | expect(body.account.displayName).to.equal('cyan') | |
158 | expect(body.email).to.equal('cyan@example.com') | |
159 | expect(body.role.id).to.equal(UserRole.USER) | |
160 | expect(body.adminFlags).to.equal(UserAdminFlag.NONE) | |
161 | expect(body.videoQuota).to.equal(5242880) | |
162 | expect(body.videoQuotaDaily).to.equal(-1) | |
163 | } | |
164 | }) | |
165 | ||
166 | it('Should auto login Kefka, create the user and use the token', async function () { | |
167 | { | |
168 | const res = await loginExternal({ | |
169 | server, | |
170 | npmName: 'test-external-auth-one', | |
171 | authName: 'external-auth-2', | |
172 | username: 'kefka' | |
173 | }) | |
174 | ||
175 | kefkaAccessToken = res.access_token | |
176 | kefkaRefreshToken = res.refresh_token | |
177 | } | |
178 | ||
179 | { | |
180 | const body = await server.users.getMyInfo({ token: kefkaAccessToken }) | |
181 | expect(body.username).to.equal('kefka') | |
182 | expect(body.account.displayName).to.equal('Kefka Palazzo') | |
183 | expect(body.email).to.equal('kefka@example.com') | |
184 | expect(body.role.id).to.equal(UserRole.ADMINISTRATOR) | |
185 | expect(body.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST) | |
186 | expect(body.videoQuota).to.equal(42000) | |
187 | expect(body.videoQuotaDaily).to.equal(42100) | |
188 | ||
189 | kefkaId = body.id | |
190 | } | |
191 | }) | |
192 | ||
193 | it('Should refresh Cyan token, but not Kefka token', async function () { | |
194 | { | |
195 | const resRefresh = await server.login.refreshToken({ refreshToken: cyanRefreshToken }) | |
196 | cyanAccessToken = resRefresh.body.access_token | |
197 | cyanRefreshToken = resRefresh.body.refresh_token | |
198 | ||
199 | const body = await server.users.getMyInfo({ token: cyanAccessToken }) | |
200 | expect(body.username).to.equal('cyan') | |
201 | } | |
202 | ||
203 | { | |
204 | await server.login.refreshToken({ refreshToken: kefkaRefreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | |
205 | } | |
206 | }) | |
207 | ||
208 | it('Should update Cyan profile', async function () { | |
209 | await server.users.updateMe({ | |
210 | token: cyanAccessToken, | |
211 | displayName: 'Cyan Garamonde', | |
212 | description: 'Retainer to the king of Doma' | |
213 | }) | |
214 | ||
215 | const body = await server.users.getMyInfo({ token: cyanAccessToken }) | |
216 | expect(body.account.displayName).to.equal('Cyan Garamonde') | |
217 | expect(body.account.description).to.equal('Retainer to the king of Doma') | |
218 | }) | |
219 | ||
220 | it('Should logout Cyan', async function () { | |
221 | await server.login.logout({ token: cyanAccessToken }) | |
222 | }) | |
223 | ||
224 | it('Should have logged out Cyan', async function () { | |
225 | await server.servers.waitUntilLog('On logout cyan') | |
226 | ||
227 | await server.users.getMyInfo({ token: cyanAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | |
228 | }) | |
229 | ||
230 | it('Should login Cyan and keep the old existing profile', async function () { | |
231 | { | |
232 | const res = await loginExternal({ | |
233 | server, | |
234 | npmName: 'test-external-auth-one', | |
235 | authName: 'external-auth-1', | |
236 | query: { | |
237 | username: 'cyan' | |
238 | }, | |
239 | username: 'cyan' | |
240 | }) | |
241 | ||
242 | cyanAccessToken = res.access_token | |
243 | } | |
244 | ||
245 | const body = await server.users.getMyInfo({ token: cyanAccessToken }) | |
246 | expect(body.username).to.equal('cyan') | |
247 | expect(body.account.displayName).to.equal('Cyan Garamonde') | |
248 | expect(body.account.description).to.equal('Retainer to the king of Doma') | |
249 | expect(body.role.id).to.equal(UserRole.USER) | |
250 | }) | |
251 | ||
252 | it('Should login Kefka and update the profile', async function () { | |
253 | { | |
254 | await server.users.update({ userId: kefkaId, videoQuota: 43000, videoQuotaDaily: 43100 }) | |
255 | await server.users.updateMe({ token: kefkaAccessToken, displayName: 'kefka updated' }) | |
256 | ||
257 | const body = await server.users.getMyInfo({ token: kefkaAccessToken }) | |
258 | expect(body.username).to.equal('kefka') | |
259 | expect(body.account.displayName).to.equal('kefka updated') | |
260 | expect(body.videoQuota).to.equal(43000) | |
261 | expect(body.videoQuotaDaily).to.equal(43100) | |
262 | } | |
263 | ||
264 | { | |
265 | const res = await loginExternal({ | |
266 | server, | |
267 | npmName: 'test-external-auth-one', | |
268 | authName: 'external-auth-2', | |
269 | username: 'kefka' | |
270 | }) | |
271 | ||
272 | kefkaAccessToken = res.access_token | |
273 | kefkaRefreshToken = res.refresh_token | |
274 | ||
275 | const body = await server.users.getMyInfo({ token: kefkaAccessToken }) | |
276 | expect(body.username).to.equal('kefka') | |
277 | expect(body.account.displayName).to.equal('Kefka Palazzo') | |
278 | expect(body.videoQuota).to.equal(42000) | |
279 | expect(body.videoQuotaDaily).to.equal(43100) | |
280 | } | |
281 | }) | |
282 | ||
283 | it('Should not update an external auth email', async function () { | |
284 | await server.users.updateMe({ | |
285 | token: cyanAccessToken, | |
286 | email: 'toto@example.com', | |
287 | currentPassword: 'toto', | |
288 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | |
289 | }) | |
290 | }) | |
291 | ||
292 | it('Should reject token of Kefka by the plugin hook', async function () { | |
293 | await wait(5000) | |
294 | ||
295 | await server.users.getMyInfo({ token: kefkaAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | |
296 | }) | |
297 | ||
298 | it('Should unregister external-auth-2 and do not login existing Kefka', async function () { | |
299 | await server.plugins.updateSettings({ | |
300 | npmName: 'peertube-plugin-test-external-auth-one', | |
301 | settings: { disableKefka: true } | |
302 | }) | |
303 | ||
304 | await server.login.login({ user: { username: 'kefka', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | |
305 | ||
306 | await loginExternal({ | |
307 | server, | |
308 | npmName: 'test-external-auth-one', | |
309 | authName: 'external-auth-2', | |
310 | query: { | |
311 | username: 'kefka' | |
312 | }, | |
313 | username: 'kefka', | |
314 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | |
315 | }) | |
316 | }) | |
317 | ||
318 | it('Should have disabled this auth', async function () { | |
319 | const config = await server.config.getConfig() | |
320 | ||
321 | const auths = config.plugin.registeredExternalAuths | |
322 | expect(auths).to.have.lengthOf(8) | |
323 | ||
324 | const auth1 = auths.find(a => a.authName === 'external-auth-2') | |
325 | expect(auth1).to.not.exist | |
326 | }) | |
327 | ||
328 | it('Should uninstall the plugin one and do not login Cyan', async function () { | |
329 | await server.plugins.uninstall({ npmName: 'peertube-plugin-test-external-auth-one' }) | |
330 | ||
331 | await loginExternal({ | |
332 | server, | |
333 | npmName: 'test-external-auth-one', | |
334 | authName: 'external-auth-1', | |
335 | query: { | |
336 | username: 'cyan' | |
337 | }, | |
338 | username: 'cyan', | |
339 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | |
340 | }) | |
341 | ||
342 | await server.login.login({ user: { username: 'cyan', password: null }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | |
343 | await server.login.login({ user: { username: 'cyan', password: '' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | |
344 | await server.login.login({ user: { username: 'cyan', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | |
345 | }) | |
346 | ||
347 | it('Should not login kefka with another plugin', async function () { | |
348 | await loginExternal({ | |
349 | server, | |
350 | npmName: 'test-external-auth-two', | |
351 | authName: 'external-auth-4', | |
352 | username: 'kefka2', | |
353 | expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400 | |
354 | }) | |
355 | ||
356 | await loginExternal({ | |
357 | server, | |
358 | npmName: 'test-external-auth-two', | |
359 | authName: 'external-auth-4', | |
360 | username: 'kefka', | |
361 | expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400 | |
362 | }) | |
363 | }) | |
364 | ||
365 | it('Should not login an existing user email', async function () { | |
366 | await server.users.create({ username: 'existing_user', password: 'super_password' }) | |
367 | ||
368 | await loginExternal({ | |
369 | server, | |
370 | npmName: 'test-external-auth-two', | |
371 | authName: 'external-auth-6', | |
372 | username: 'existing_user', | |
373 | expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400 | |
374 | }) | |
375 | }) | |
376 | ||
377 | it('Should be able to login an existing user username and channel', async function () { | |
378 | await server.users.create({ username: 'existing_user2' }) | |
379 | await server.users.create({ username: 'existing_user2-1_channel' }) | |
380 | ||
381 | // Test twice to ensure we don't generate a username on every login | |
382 | for (let i = 0; i < 2; i++) { | |
383 | const res = await loginExternal({ | |
384 | server, | |
385 | npmName: 'test-external-auth-two', | |
386 | authName: 'external-auth-7', | |
387 | username: 'existing_user2' | |
388 | }) | |
389 | ||
390 | const token = res.access_token | |
391 | ||
392 | const myInfo = await server.users.getMyInfo({ token }) | |
393 | expect(myInfo.username).to.equal('existing_user2-1') | |
394 | ||
395 | expect(myInfo.videoChannels[0].name).to.equal('existing_user2-1_channel-1') | |
396 | } | |
397 | }) | |
398 | ||
399 | it('Should display the correct configuration', async function () { | |
400 | const config = await server.config.getConfig() | |
401 | ||
402 | const auths = config.plugin.registeredExternalAuths | |
403 | expect(auths).to.have.lengthOf(7) | |
404 | ||
405 | const auth2 = auths.find((a) => a.authName === 'external-auth-2') | |
406 | expect(auth2).to.not.exist | |
407 | }) | |
408 | ||
409 | after(async function () { | |
410 | await cleanupTests([ server ]) | |
411 | }) | |
412 | ||
413 | it('Should forward the redirectUrl if the plugin returns one', async function () { | |
414 | const resLogin = await loginExternal({ | |
415 | server, | |
416 | npmName: 'test-external-auth-three', | |
417 | authName: 'external-auth-7', | |
418 | username: 'cid' | |
419 | }) | |
420 | ||
421 | const { redirectUrl } = await server.login.logout({ token: resLogin.access_token }) | |
422 | expect(redirectUrl).to.equal('https://example.com/redirectUrl') | |
423 | }) | |
424 | ||
425 | it('Should call the plugin\'s onLogout method with the request', async function () { | |
426 | const resLogin = await loginExternal({ | |
427 | server, | |
428 | npmName: 'test-external-auth-three', | |
429 | authName: 'external-auth-8', | |
430 | username: 'cid' | |
431 | }) | |
432 | ||
433 | const { redirectUrl } = await server.login.logout({ token: resLogin.access_token }) | |
434 | expect(redirectUrl).to.equal('https://example.com/redirectUrl?access_token=' + resLogin.access_token) | |
435 | }) | |
436 | }) |