diff options
Diffstat (limited to 'packages/tests/src/plugins/external-auth.ts')
-rw-r--r-- | packages/tests/src/plugins/external-auth.ts | 436 |
1 files changed, 436 insertions, 0 deletions
diff --git a/packages/tests/src/plugins/external-auth.ts b/packages/tests/src/plugins/external-auth.ts new file mode 100644 index 000000000..c7fe22185 --- /dev/null +++ b/packages/tests/src/plugins/external-auth.ts | |||
@@ -0,0 +1,436 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, HttpStatusCodeType, UserAdminFlag, UserRole } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | decodeQueryString, | ||
10 | PeerTubeServer, | ||
11 | PluginsCommand, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-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?: HttpStatusCodeType | ||
22 | expectedStatusStep2?: HttpStatusCodeType | ||
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 | }) | ||