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