]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/tests/plugins/external-auth.ts
e4015939a986192adc6191eaaf89c09f1fd1eefc
[github/Chocobozzz/PeerTube.git] / server / tests / plugins / external-auth.ts
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 })