external-auth.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
  2. import 'mocha'
  3. import { expect } from 'chai'
  4. import { wait } from '@shared/core-utils'
  5. import { HttpStatusCode, UserRole } from '@shared/models'
  6. import {
  7. cleanupTests,
  8. createSingleServer,
  9. decodeQueryString,
  10. PeerTubeServer,
  11. PluginsCommand,
  12. setAccessTokensToServers
  13. } from '@shared/server-commands'
  14. async function loginExternal (options: {
  15. server: PeerTubeServer
  16. npmName: string
  17. authName: string
  18. username: string
  19. query?: any
  20. expectedStatus?: HttpStatusCode
  21. expectedStatusStep2?: HttpStatusCode
  22. }) {
  23. const res = await options.server.plugins.getExternalAuth({
  24. npmName: options.npmName,
  25. npmVersion: '0.0.1',
  26. authName: options.authName,
  27. query: options.query,
  28. expectedStatus: options.expectedStatus || HttpStatusCode.FOUND_302
  29. })
  30. if (res.status !== HttpStatusCode.FOUND_302) return
  31. const location = res.header.location
  32. const { externalAuthToken } = decodeQueryString(location)
  33. const resLogin = await options.server.login.loginUsingExternalToken({
  34. username: options.username,
  35. externalAuthToken: externalAuthToken as string,
  36. expectedStatus: options.expectedStatusStep2
  37. })
  38. return resLogin.body
  39. }
  40. describe('Test external auth plugins', function () {
  41. let server: PeerTubeServer
  42. let cyanAccessToken: string
  43. let cyanRefreshToken: string
  44. let kefkaAccessToken: string
  45. let kefkaRefreshToken: string
  46. let externalAuthToken: string
  47. before(async function () {
  48. this.timeout(30000)
  49. server = await createSingleServer(1)
  50. await setAccessTokensToServers([ server ])
  51. for (const suffix of [ 'one', 'two', 'three' ]) {
  52. await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-external-auth-' + suffix) })
  53. }
  54. })
  55. it('Should display the correct configuration', async function () {
  56. const config = await server.config.getConfig()
  57. const auths = config.plugin.registeredExternalAuths
  58. expect(auths).to.have.lengthOf(8)
  59. const auth2 = auths.find((a) => a.authName === 'external-auth-2')
  60. expect(auth2).to.exist
  61. expect(auth2.authDisplayName).to.equal('External Auth 2')
  62. expect(auth2.npmName).to.equal('peertube-plugin-test-external-auth-one')
  63. })
  64. it('Should redirect for a Cyan login', async function () {
  65. const res = await server.plugins.getExternalAuth({
  66. npmName: 'test-external-auth-one',
  67. npmVersion: '0.0.1',
  68. authName: 'external-auth-1',
  69. query: {
  70. username: 'cyan'
  71. },
  72. expectedStatus: HttpStatusCode.FOUND_302
  73. })
  74. const location = res.header.location
  75. expect(location.startsWith('/login?')).to.be.true
  76. const searchParams = decodeQueryString(location)
  77. expect(searchParams.externalAuthToken).to.exist
  78. expect(searchParams.username).to.equal('cyan')
  79. externalAuthToken = searchParams.externalAuthToken as string
  80. })
  81. it('Should reject auto external login with a missing or invalid token', async function () {
  82. const command = server.login
  83. await command.loginUsingExternalToken({ username: 'cyan', externalAuthToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  84. await command.loginUsingExternalToken({ username: 'cyan', externalAuthToken: 'blabla', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  85. })
  86. it('Should reject auto external login with a missing or invalid username', async function () {
  87. const command = server.login
  88. await command.loginUsingExternalToken({ username: '', externalAuthToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  89. await command.loginUsingExternalToken({ username: '', externalAuthToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  90. })
  91. it('Should reject auto external login with an expired token', async function () {
  92. this.timeout(15000)
  93. await wait(5000)
  94. await server.login.loginUsingExternalToken({
  95. username: 'cyan',
  96. externalAuthToken,
  97. expectedStatus: HttpStatusCode.BAD_REQUEST_400
  98. })
  99. await server.servers.waitUntilLog('expired external auth token', 4)
  100. })
  101. it('Should auto login Cyan, create the user and use the token', async function () {
  102. {
  103. const res = await loginExternal({
  104. server,
  105. npmName: 'test-external-auth-one',
  106. authName: 'external-auth-1',
  107. query: {
  108. username: 'cyan'
  109. },
  110. username: 'cyan'
  111. })
  112. cyanAccessToken = res.access_token
  113. cyanRefreshToken = res.refresh_token
  114. }
  115. {
  116. const body = await server.users.getMyInfo({ token: cyanAccessToken })
  117. expect(body.username).to.equal('cyan')
  118. expect(body.account.displayName).to.equal('cyan')
  119. expect(body.email).to.equal('cyan@example.com')
  120. expect(body.role).to.equal(UserRole.USER)
  121. }
  122. })
  123. it('Should auto login Kefka, create the user and use the token', async function () {
  124. {
  125. const res = await loginExternal({
  126. server,
  127. npmName: 'test-external-auth-one',
  128. authName: 'external-auth-2',
  129. username: 'kefka'
  130. })
  131. kefkaAccessToken = res.access_token
  132. kefkaRefreshToken = res.refresh_token
  133. }
  134. {
  135. const body = await server.users.getMyInfo({ token: kefkaAccessToken })
  136. expect(body.username).to.equal('kefka')
  137. expect(body.account.displayName).to.equal('Kefka Palazzo')
  138. expect(body.email).to.equal('kefka@example.com')
  139. expect(body.role).to.equal(UserRole.ADMINISTRATOR)
  140. }
  141. })
  142. it('Should refresh Cyan token, but not Kefka token', async function () {
  143. {
  144. const resRefresh = await server.login.refreshToken({ refreshToken: cyanRefreshToken })
  145. cyanAccessToken = resRefresh.body.access_token
  146. cyanRefreshToken = resRefresh.body.refresh_token
  147. const body = await server.users.getMyInfo({ token: cyanAccessToken })
  148. expect(body.username).to.equal('cyan')
  149. }
  150. {
  151. await server.login.refreshToken({ refreshToken: kefkaRefreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  152. }
  153. })
  154. it('Should update Cyan profile', async function () {
  155. await server.users.updateMe({
  156. token: cyanAccessToken,
  157. displayName: 'Cyan Garamonde',
  158. description: 'Retainer to the king of Doma'
  159. })
  160. const body = await server.users.getMyInfo({ token: cyanAccessToken })
  161. expect(body.account.displayName).to.equal('Cyan Garamonde')
  162. expect(body.account.description).to.equal('Retainer to the king of Doma')
  163. })
  164. it('Should logout Cyan', async function () {
  165. await server.login.logout({ token: cyanAccessToken })
  166. })
  167. it('Should have logged out Cyan', async function () {
  168. await server.servers.waitUntilLog('On logout cyan')
  169. await server.users.getMyInfo({ token: cyanAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
  170. })
  171. it('Should login Cyan and keep the old existing profile', async function () {
  172. {
  173. const res = await loginExternal({
  174. server,
  175. npmName: 'test-external-auth-one',
  176. authName: 'external-auth-1',
  177. query: {
  178. username: 'cyan'
  179. },
  180. username: 'cyan'
  181. })
  182. cyanAccessToken = res.access_token
  183. }
  184. const body = await server.users.getMyInfo({ token: cyanAccessToken })
  185. expect(body.username).to.equal('cyan')
  186. expect(body.account.displayName).to.equal('Cyan Garamonde')
  187. expect(body.account.description).to.equal('Retainer to the king of Doma')
  188. expect(body.role).to.equal(UserRole.USER)
  189. })
  190. it('Should not update an external auth email', async function () {
  191. await server.users.updateMe({
  192. token: cyanAccessToken,
  193. email: 'toto@example.com',
  194. currentPassword: 'toto',
  195. expectedStatus: HttpStatusCode.BAD_REQUEST_400
  196. })
  197. })
  198. it('Should reject token of Kefka by the plugin hook', async function () {
  199. this.timeout(10000)
  200. await wait(5000)
  201. await server.users.getMyInfo({ token: kefkaAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
  202. })
  203. it('Should unregister external-auth-2 and do not login existing Kefka', async function () {
  204. await server.plugins.updateSettings({
  205. npmName: 'peertube-plugin-test-external-auth-one',
  206. settings: { disableKefka: true }
  207. })
  208. await server.login.login({ user: { username: 'kefka', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  209. await loginExternal({
  210. server,
  211. npmName: 'test-external-auth-one',
  212. authName: 'external-auth-2',
  213. query: {
  214. username: 'kefka'
  215. },
  216. username: 'kefka',
  217. expectedStatus: HttpStatusCode.NOT_FOUND_404
  218. })
  219. })
  220. it('Should have disabled this auth', async function () {
  221. const config = await server.config.getConfig()
  222. const auths = config.plugin.registeredExternalAuths
  223. expect(auths).to.have.lengthOf(7)
  224. const auth1 = auths.find(a => a.authName === 'external-auth-2')
  225. expect(auth1).to.not.exist
  226. })
  227. it('Should uninstall the plugin one and do not login Cyan', async function () {
  228. await server.plugins.uninstall({ npmName: 'peertube-plugin-test-external-auth-one' })
  229. await loginExternal({
  230. server,
  231. npmName: 'test-external-auth-one',
  232. authName: 'external-auth-1',
  233. query: {
  234. username: 'cyan'
  235. },
  236. username: 'cyan',
  237. expectedStatus: HttpStatusCode.NOT_FOUND_404
  238. })
  239. await server.login.login({ user: { username: 'cyan', password: null }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  240. await server.login.login({ user: { username: 'cyan', password: '' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  241. await server.login.login({ user: { username: 'cyan', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
  242. })
  243. it('Should not login kefka with another plugin', async function () {
  244. await loginExternal({
  245. server,
  246. npmName: 'test-external-auth-two',
  247. authName: 'external-auth-4',
  248. username: 'kefka2',
  249. expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400
  250. })
  251. await loginExternal({
  252. server,
  253. npmName: 'test-external-auth-two',
  254. authName: 'external-auth-4',
  255. username: 'kefka',
  256. expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400
  257. })
  258. })
  259. it('Should not login an existing user', async function () {
  260. await server.users.create({ username: 'existing_user', password: 'super_password' })
  261. await loginExternal({
  262. server,
  263. npmName: 'test-external-auth-two',
  264. authName: 'external-auth-6',
  265. username: 'existing_user',
  266. expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400
  267. })
  268. })
  269. it('Should display the correct configuration', async function () {
  270. const config = await server.config.getConfig()
  271. const auths = config.plugin.registeredExternalAuths
  272. expect(auths).to.have.lengthOf(6)
  273. const auth2 = auths.find((a) => a.authName === 'external-auth-2')
  274. expect(auth2).to.not.exist
  275. })
  276. after(async function () {
  277. await cleanupTests([ server ])
  278. })
  279. it('Should forward the redirectUrl if the plugin returns one', async function () {
  280. const resLogin = await loginExternal({
  281. server,
  282. npmName: 'test-external-auth-three',
  283. authName: 'external-auth-7',
  284. username: 'cid'
  285. })
  286. const { redirectUrl } = await server.login.logout({ token: resLogin.access_token })
  287. expect(redirectUrl).to.equal('https://example.com/redirectUrl')
  288. })
  289. it('Should call the plugin\'s onLogout method with the request', async function () {
  290. const resLogin = await loginExternal({
  291. server,
  292. npmName: 'test-external-auth-three',
  293. authName: 'external-auth-8',
  294. username: 'cid'
  295. })
  296. const { redirectUrl } = await server.login.logout({ token: resLogin.access_token })
  297. expect(redirectUrl).to.equal('https://example.com/redirectUrl?access_token=' + resLogin.access_token)
  298. })
  299. })