Add external login tests
authorChocobozzz <me@florianbigard.com>
Wed, 29 Apr 2020 07:04:42 +0000 (09:04 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Mon, 4 May 2020 14:21:39 +0000 (16:21 +0200)
19 files changed:
server/controllers/api/users/token.ts
server/initializers/constants.ts
server/lib/auth.ts
server/lib/oauth-model.ts
server/lib/plugins/plugin-manager.ts
server/lib/plugins/register-helpers-store.ts
server/tests/api/check-params/plugins.ts
server/tests/external-plugins/auth-ldap.ts
server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js [new file with mode: 0644]
server/tests/fixtures/peertube-plugin-test-external-auth-one/package.json [new file with mode: 0644]
server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js [new file with mode: 0644]
server/tests/fixtures/peertube-plugin-test-external-auth-two/package.json [new file with mode: 0644]
server/tests/plugins/external-auth.ts [new file with mode: 0644]
server/tests/plugins/id-and-pass-auth.ts
server/tests/plugins/index.ts
shared/extra-utils/requests/requests.ts
shared/extra-utils/server/plugins.ts
shared/extra-utils/users/login.ts
shared/models/plugins/register-server-auth.model.ts

index f4be228f62f9eb0d84d41112bf6d41396b61aad8..41aa2676916bf0c0656d717b6fd123ce9836d8e4 100644 (file)
@@ -1,4 +1,4 @@
-import { handleIdAndPassLogin, handleTokenRevocation } from '@server/lib/auth'
+import { handleLogin, handleTokenRevocation } from '@server/lib/auth'
 import * as RateLimit from 'express-rate-limit'
 import { CONFIG } from '@server/initializers/config'
 import * as express from 'express'
@@ -14,7 +14,7 @@ const loginRateLimiter = RateLimit({
 
 tokensRouter.post('/token',
   loginRateLimiter,
-  handleIdAndPassLogin,
+  handleLogin,
   tokenSuccess
 )
 
index c8623a5d43cf31cfb4884b0823afe258059c995e..c8e50dd53b681b01ae2152f04fe92c4fce494588 100644 (file)
@@ -645,6 +645,8 @@ const P2P_MEDIA_LOADER_PEER_VERSION = 2
 const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css'
 const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME)
 
+let PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 1000 * 60 * 5 // 5 minutes
+
 const DEFAULT_THEME_NAME = 'default'
 const DEFAULT_USER_THEME_NAME = 'instance-default'
 
@@ -686,6 +688,8 @@ if (isTestInstance() === true) {
   FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000
   MEMOIZE_TTL.OVERVIEWS_SAMPLE = 3000
   OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD = 2
+
+  PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000
 }
 
 updateWebserverUrls()
@@ -778,6 +782,7 @@ export {
   VIDEO_VIEW_LIFETIME,
   CONTACT_FORM_LIFETIME,
   VIDEO_PLAYLIST_PRIVACIES,
+  PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME,
   ASSETS_PATH,
   loadLanguages,
   buildLanguages
index eaae5fdf3d36d00564cd6af06badcc902f16e945..2ef77bc9ca120d7bc05f374ecf55aa3638adeb9b 100644 (file)
@@ -1,7 +1,7 @@
 import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
 import { logger } from '@server/helpers/logger'
 import { generateRandomString } from '@server/helpers/utils'
-import { OAUTH_LIFETIME, WEBSERVER } from '@server/initializers/constants'
+import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants'
 import { revokeToken } from '@server/lib/oauth-model'
 import { PluginManager } from '@server/lib/plugins/plugin-manager'
 import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
@@ -35,7 +35,7 @@ const authBypassTokens = new Map<string, {
   npmName: string
 }>()
 
-async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
+async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
   const grantType = req.body.grant_type
 
   if (grantType === 'password') {
@@ -90,10 +90,9 @@ async function onExternalUserAuthenticated (options: {
   logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName)
 
   const bypassToken = await generateRandomString(32)
-  const tokenLifetime = 1000 * 60 * 5 // 5 minutes
 
   const expires = new Date()
-  expires.setTime(expires.getTime() + tokenLifetime)
+  expires.setTime(expires.getTime() + PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME)
 
   const user = buildUserResult(authResult)
   authBypassTokens.set(bypassToken, {
@@ -108,7 +107,7 @@ async function onExternalUserAuthenticated (options: {
 
 // ---------------------------------------------------------------------------
 
-export { oAuthServer, handleIdAndPassLogin, onExternalUserAuthenticated, handleTokenRevocation }
+export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation }
 
 // ---------------------------------------------------------------------------
 
@@ -212,7 +211,7 @@ function proxifyExternalAuthBypass (req: express.Request, res: express.Response)
 
   const now = new Date()
   if (now.getTime() > expires.getTime()) {
-    logger.error('Cannot authenticate user with an expired bypass token')
+    logger.error('Cannot authenticate user with an expired external auth token')
     return res.sendStatus(400)
   }
 
@@ -267,7 +266,7 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
   return {
     username: pluginResult.username,
     email: pluginResult.email,
-    role: pluginResult.role || UserRole.USER,
+    role: pluginResult.role ?? UserRole.USER,
     displayName: pluginResult.displayName || pluginResult.username
   }
 }
index 8b9975bb4c331834f50c59346bfe568553144862..8d8a6d85e60bb9a9e92c93d88f089e02f2f7b513 100644 (file)
@@ -139,7 +139,7 @@ async function revokeToken (tokenInfo: { refreshToken: string }) {
 
   if (token) {
     if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) {
-      PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName)
+      PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User)
     }
 
     clearCacheByToken(token.accessToken)
index c64ca60aaebe7a05dd5f1c0c9a712f9acab76dec..38336bcc64eda8a24fd76b4385fdcd8a19bcc842 100644 (file)
@@ -21,7 +21,8 @@ import { ClientHtml } from '../client-html'
 import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model'
 import { RegisterHelpersStore } from './register-helpers-store'
 import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
-import { MOAuthTokenUser } from '@server/typings/models'
+import { MOAuthTokenUser, MUser } from '@server/typings/models'
+import { RegisterServerAuthPassOptions, RegisterServerAuthExternalOptions } from '@shared/models/plugins/register-server-auth.model'
 
 export interface RegisteredPlugin {
   npmName: string
@@ -133,14 +134,14 @@ export class PluginManager implements ServerHook {
     return this.translations[locale] || {}
   }
 
-  onLogout (npmName: string, authName: string) {
+  onLogout (npmName: string, authName: string, user: MUser) {
     const auth = this.getAuth(npmName, authName)
 
     if (auth?.onLogout) {
       logger.info('Running onLogout function from auth %s of plugin %s', authName, npmName)
 
       try {
-        auth.onLogout()
+        auth.onLogout(user)
       } catch (err) {
         logger.warn('Cannot run onLogout function from auth %s of plugin %s.', authName, npmName, { err })
       }
@@ -478,8 +479,10 @@ export class PluginManager implements ServerHook {
     const plugin = this.getRegisteredPluginOrTheme(npmName)
     if (!plugin || plugin.type !== PluginType.PLUGIN) return null
 
-    return plugin.registerHelpersStore.getIdAndPassAuths()
-                 .find(a => a.authName === authName)
+    let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpersStore.getIdAndPassAuths()
+    auths = auths.concat(plugin.registerHelpersStore.getExternalAuths())
+
+    return auths.find(a => a.authName === authName)
   }
 
   // ###################### Private getters ######################
index 277f2b687aab917e7c9ee9d3af700c4057254d28..151196bf19eaa12be4c5e92e0865b3ba1cd6a650 100644 (file)
@@ -1,5 +1,12 @@
+import * as express from 'express'
 import { logger } from '@server/helpers/logger'
-import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PRIVACIES } from '@server/initializers/constants'
+import {
+  VIDEO_CATEGORIES,
+  VIDEO_LANGUAGES,
+  VIDEO_LICENCES,
+  VIDEO_PLAYLIST_PRIVACIES,
+  VIDEO_PRIVACIES
+} from '@server/initializers/constants'
 import { onExternalUserAuthenticated } from '@server/lib/auth'
 import { PluginModel } from '@server/models/server/plugin'
 import { RegisterServerOptions } from '@server/typings/plugins'
@@ -10,11 +17,15 @@ import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video-
 import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model'
 import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model'
 import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model'
-import { RegisterServerAuthExternalOptions, RegisterServerAuthExternalResult, RegisterServerAuthPassOptions, RegisterServerExternalAuthenticatedResult } from '@shared/models/plugins/register-server-auth.model'
+import {
+  RegisterServerAuthExternalOptions,
+  RegisterServerAuthExternalResult,
+  RegisterServerAuthPassOptions,
+  RegisterServerExternalAuthenticatedResult
+} from '@shared/models/plugins/register-server-auth.model'
 import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
 import { RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model'
 import { serverHookObject } from '@shared/models/plugins/server-hook.model'
-import * as express from 'express'
 import { buildPluginHelpers } from './plugin-helpers'
 
 type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
@@ -174,6 +185,11 @@ export class RegisterHelpersStore {
     const self = this
 
     return (options: RegisterServerAuthExternalOptions) => {
+      if (!options.authName || !options.onAuthRequest || typeof options.onAuthRequest !== 'function') {
+        logger.error('Cannot register auth plugin %s: authName of getWeight or login are not valid.', this.npmName)
+        return
+      }
+
       this.externalAuths.push(options)
 
       return {
index cf80b35c297e6217c43b02b95df4c6291c677cec..07ded26eeedeebbd3a03c0ee6575440f5f85ed40 100644 (file)
@@ -64,6 +64,7 @@ describe('Test server plugins API validators', function () {
   describe('With static plugin routes', function () {
     it('Should fail with an unknown plugin name/plugin version', async function () {
       const paths = [
+        '/plugins/' + pluginName + '/0.0.1/auth/fake-auth',
         '/plugins/' + pluginName + '/0.0.1/static/images/chocobo.png',
         '/plugins/' + pluginName + '/0.0.1/client-scripts/client/common-client-plugin.js',
         '/themes/' + themeName + '/0.0.1/static/images/chocobo.png',
@@ -86,6 +87,7 @@ describe('Test server plugins API validators', function () {
 
     it('Should fail with invalid versions', async function () {
       const paths = [
+        '/plugins/' + pluginName + '/0.0.1.1/auth/fake-auth',
         '/plugins/' + pluginName + '/0.0.1.1/static/images/chocobo.png',
         '/plugins/' + pluginName + '/0.1/client-scripts/client/common-client-plugin.js',
         '/themes/' + themeName + '/1/static/images/chocobo.png',
@@ -112,6 +114,12 @@ describe('Test server plugins API validators', function () {
       }
     })
 
+    it('Should fail with an unknown auth name', async function () {
+      const path = '/plugins/' + pluginName + '/' + npmVersion + '/auth/bad-auth'
+
+      await makeGetRequest({ url: server.url, path, statusCodeExpected: 404 })
+    })
+
     it('Should fail with an unknown static file', async function () {
       const paths = [
         '/plugins/' + pluginName + '/' + npmVersion + '/static/fake/chocobo.png',
@@ -145,6 +153,9 @@ describe('Test server plugins API validators', function () {
       for (const p of paths) {
         await makeGetRequest({ url: server.url, path: p, statusCodeExpected: 200 })
       }
+
+      const authPath = '/plugins/' + pluginName + '/' + npmVersion + '/auth/fake-auth'
+      await makeGetRequest({ url: server.url, path: authPath, statusCodeExpected: 302 })
     })
   })
 
@@ -462,6 +473,8 @@ describe('Test server plugins API validators', function () {
     })
 
     it('Should succeed with the correct parameters', async function () {
+      this.timeout(10000)
+
       const it = [
         { suffix: 'install', status: 200 },
         { suffix: 'update', status: 200 },
index 7aee986c78e52e41b9f1ae7cf137c9a7ea4f7906..0f0a085324b0633e1277179597929fe9a84eec2a 100644 (file)
@@ -1,10 +1,18 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import { getMyUserInformation, installPlugin, setAccessTokensToServers, updatePluginSettings, userLogin, uploadVideo, uninstallPlugin } from '../../../shared/extra-utils'
-import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
-import { User } from '@shared/models/users/user.model'
 import { expect } from 'chai'
+import { User } from '@shared/models/users/user.model'
+import {
+  getMyUserInformation,
+  installPlugin,
+  setAccessTokensToServers,
+  uninstallPlugin,
+  updatePluginSettings,
+  uploadVideo,
+  userLogin
+} from '../../../shared/extra-utils'
+import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
 
 describe('Official plugin auth-ldap', function () {
   let server: ServerInfo
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js b/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js
new file mode 100644 (file)
index 0000000..f29fd1f
--- /dev/null
@@ -0,0 +1,67 @@
+async function register ({
+  registerExternalAuth,
+  peertubeHelpers
+}) {
+  {
+    const result = registerExternalAuth({
+      authName: 'external-auth-1',
+      authDisplayName: 'External Auth 1',
+      onLogout: user => peertubeHelpers.logger.info('On logout %s', user.username),
+      onAuthRequest: (req, res) => {
+        const username = req.query.username
+
+        result.userAuthenticated({
+          req,
+          res,
+          username,
+          email: username + '@example.com'
+        })
+      }
+    })
+  }
+
+  {
+    const result = registerExternalAuth({
+      authName: 'external-auth-2',
+      authDisplayName: 'External Auth 2',
+      onAuthRequest: (req, res) => {
+        result.userAuthenticated({
+          req,
+          res,
+          username: 'kefka',
+          email: 'kefka@example.com',
+          role: 0,
+          displayName: 'Kefka Palazzo'
+        })
+      },
+      hookTokenValidity: (options) => {
+        if (options.type === 'refresh') {
+          return { valid: false }
+        }
+
+        if (options.type === 'access') {
+          const token = options.token
+          const now = new Date()
+          now.setTime(now.getTime() - 5000)
+
+          const createdAt = new Date(token.createdAt)
+
+          return { valid: createdAt.getTime() >= now.getTime() }
+        }
+
+        return { valid: true }
+      }
+    })
+  }
+}
+
+async function unregister () {
+  return
+}
+
+module.exports = {
+  register,
+  unregister
+}
+
+// ###########################################################################
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-one/package.json b/server/tests/fixtures/peertube-plugin-test-external-auth-one/package.json
new file mode 100644 (file)
index 0000000..22814b0
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "name": "peertube-plugin-test-external-auth-one",
+  "version": "0.0.1",
+  "description": "External auth one",
+  "engine": {
+    "peertube": ">=1.3.0"
+  },
+  "keywords": [
+    "peertube",
+    "plugin"
+  ],
+  "homepage": "https://github.com/Chocobozzz/PeerTube",
+  "author": "Chocobozzz",
+  "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
+  "library": "./main.js",
+  "staticDirs": {},
+  "css": [],
+  "clientScripts": [],
+  "translations": {}
+}
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js b/server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js
new file mode 100644 (file)
index 0000000..34fec1b
--- /dev/null
@@ -0,0 +1,31 @@
+async function register ({
+  registerExternalAuth,
+  peertubeHelpers
+}) {
+  {
+    const result = registerExternalAuth({
+      authName: 'external-auth-3',
+      authDisplayName: 'External Auth 3',
+      onAuthRequest: (req, res) => {
+        result.userAuthenticated({
+          req,
+          res,
+          username: 'cid',
+          email: 'cid@example.com',
+          displayName: 'Cid Marquez'
+        })
+      }
+    })
+  }
+}
+
+async function unregister () {
+  return
+}
+
+module.exports = {
+  register,
+  unregister
+}
+
+// ###########################################################################
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-two/package.json b/server/tests/fixtures/peertube-plugin-test-external-auth-two/package.json
new file mode 100644 (file)
index 0000000..a5ca4d0
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "name": "peertube-plugin-test-external-auth-two",
+  "version": "0.0.1",
+  "description": "External auth two",
+  "engine": {
+    "peertube": ">=1.3.0"
+  },
+  "keywords": [
+    "peertube",
+    "plugin"
+  ],
+  "homepage": "https://github.com/Chocobozzz/PeerTube",
+  "author": "Chocobozzz",
+  "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
+  "library": "./main.js",
+  "staticDirs": {},
+  "css": [],
+  "clientScripts": [],
+  "translations": {}
+}
diff --git a/server/tests/plugins/external-auth.ts b/server/tests/plugins/external-auth.ts
new file mode 100644 (file)
index 0000000..a72b282
--- /dev/null
@@ -0,0 +1,295 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import 'mocha'
+import { expect } from 'chai'
+import { ServerConfig, User, UserRole } from '@shared/models'
+import {
+  decodeQueryString,
+  getConfig,
+  getExternalAuth,
+  getMyUserInformation,
+  getPluginTestPath,
+  installPlugin,
+  loginUsingExternalToken,
+  logout,
+  refreshToken,
+  setAccessTokensToServers,
+  uninstallPlugin,
+  updateMyUser,
+  wait
+} from '../../../shared/extra-utils'
+import { cleanupTests, flushAndRunServer, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
+
+async function loginExternal (options: {
+  server: ServerInfo
+  npmName: string
+  authName: string
+  username: string
+  query?: any
+  statusCodeExpected?: number
+}) {
+  const res = await getExternalAuth({
+    url: options.server.url,
+    npmName: options.npmName,
+    npmVersion: '0.0.1',
+    authName: options.authName,
+    query: options.query,
+    statusCodeExpected: options.statusCodeExpected || 302
+  })
+
+  if (res.status !== 302) return
+
+  const location = res.header.location
+  const { externalAuthToken } = decodeQueryString(location)
+
+  const resLogin = await loginUsingExternalToken(
+    options.server,
+    options.username,
+    externalAuthToken as string
+  )
+
+  return resLogin.body
+}
+
+describe('Test external auth plugins', function () {
+  let server: ServerInfo
+
+  let cyanAccessToken: string
+  let cyanRefreshToken: string
+
+  let kefkaAccessToken: string
+  let kefkaRefreshToken: string
+
+  let externalAuthToken: string
+
+  before(async function () {
+    this.timeout(30000)
+
+    server = await flushAndRunServer(1)
+    await setAccessTokensToServers([ server ])
+
+    for (const suffix of [ 'one', 'two' ]) {
+      await installPlugin({
+        url: server.url,
+        accessToken: server.accessToken,
+        path: getPluginTestPath('-external-auth-' + suffix)
+      })
+    }
+  })
+
+  it('Should display the correct configuration', async function () {
+    const res = await getConfig(server.url)
+
+    const config: ServerConfig = res.body
+
+    const auths = config.plugin.registeredExternalAuths
+    expect(auths).to.have.lengthOf(3)
+
+    const auth2 = auths.find((a) => a.authName === 'external-auth-2')
+    expect(auth2).to.exist
+    expect(auth2.authDisplayName).to.equal('External Auth 2')
+    expect(auth2.npmName).to.equal('peertube-plugin-test-external-auth-one')
+  })
+
+  it('Should redirect for a Cyan login', async function () {
+    const res = await getExternalAuth({
+      url: server.url,
+      npmName: 'test-external-auth-one',
+      npmVersion: '0.0.1',
+      authName: 'external-auth-1',
+      query: {
+        username: 'cyan'
+      },
+      statusCodeExpected: 302
+    })
+
+    const location = res.header.location
+    expect(location.startsWith('/login?')).to.be.true
+
+    const searchParams = decodeQueryString(location)
+
+    expect(searchParams.externalAuthToken).to.exist
+    expect(searchParams.username).to.equal('cyan')
+
+    externalAuthToken = searchParams.externalAuthToken as string
+  })
+
+  it('Should reject auto external login with a missing or invalid token', async function () {
+    await loginUsingExternalToken(server, 'cyan', '', 400)
+    await loginUsingExternalToken(server, 'cyan', 'blabla', 400)
+  })
+
+  it('Should reject auto external login with a missing or invalid username', async function () {
+    await loginUsingExternalToken(server, '', externalAuthToken, 400)
+    await loginUsingExternalToken(server, '', externalAuthToken, 400)
+  })
+
+  it('Should reject auto external login with an expired token', async function () {
+    this.timeout(15000)
+
+    await wait(5000)
+
+    await loginUsingExternalToken(server, 'cyan', externalAuthToken, 400)
+
+    await waitUntilLog(server, 'expired external auth token')
+  })
+
+  it('Should auto login Cyan, create the user and use the token', async function () {
+    {
+      const res = await loginExternal({
+        server,
+        npmName: 'test-external-auth-one',
+        authName: 'external-auth-1',
+        query: {
+          username: 'cyan'
+        },
+        username: 'cyan'
+      })
+
+      cyanAccessToken = res.access_token
+      cyanRefreshToken = res.refresh_token
+    }
+
+    {
+      const res = await getMyUserInformation(server.url, cyanAccessToken)
+
+      const body: User = res.body
+      expect(body.username).to.equal('cyan')
+      expect(body.account.displayName).to.equal('cyan')
+      expect(body.email).to.equal('cyan@example.com')
+      expect(body.role).to.equal(UserRole.USER)
+    }
+  })
+
+  it('Should auto login Kefka, create the user and use the token', async function () {
+    {
+      const res = await loginExternal({
+        server,
+        npmName: 'test-external-auth-one',
+        authName: 'external-auth-2',
+        username: 'kefka'
+      })
+
+      kefkaAccessToken = res.access_token
+      kefkaRefreshToken = res.refresh_token
+    }
+
+    {
+      const res = await getMyUserInformation(server.url, kefkaAccessToken)
+
+      const body: User = res.body
+      expect(body.username).to.equal('kefka')
+      expect(body.account.displayName).to.equal('Kefka Palazzo')
+      expect(body.email).to.equal('kefka@example.com')
+      expect(body.role).to.equal(UserRole.ADMINISTRATOR)
+    }
+  })
+
+  it('Should refresh Cyan token, but not Kefka token', async function () {
+    {
+      const resRefresh = await refreshToken(server, cyanRefreshToken)
+      cyanAccessToken = resRefresh.body.access_token
+      cyanRefreshToken = resRefresh.body.refresh_token
+
+      const res = await getMyUserInformation(server.url, cyanAccessToken)
+      const user: User = res.body
+      expect(user.username).to.equal('cyan')
+    }
+
+    {
+      await refreshToken(server, kefkaRefreshToken, 400)
+    }
+  })
+
+  it('Should update Cyan profile', async function () {
+    await updateMyUser({
+      url: server.url,
+      accessToken: cyanAccessToken,
+      displayName: 'Cyan Garamonde',
+      description: 'Retainer to the king of Doma'
+    })
+
+    const res = await getMyUserInformation(server.url, cyanAccessToken)
+
+    const body: User = res.body
+    expect(body.account.displayName).to.equal('Cyan Garamonde')
+    expect(body.account.description).to.equal('Retainer to the king of Doma')
+  })
+
+  it('Should logout Cyan', async function () {
+    await logout(server.url, cyanAccessToken)
+  })
+
+  it('Should have logged out Cyan', async function () {
+    await waitUntilLog(server, 'On logout cyan')
+
+    await getMyUserInformation(server.url, cyanAccessToken, 401)
+  })
+
+  it('Should login Cyan and keep the old existing profile', async function () {
+    {
+      const res = await loginExternal({
+        server,
+        npmName: 'test-external-auth-one',
+        authName: 'external-auth-1',
+        query: {
+          username: 'cyan'
+        },
+        username: 'cyan'
+      })
+
+      cyanAccessToken = res.access_token
+    }
+
+    const res = await getMyUserInformation(server.url, cyanAccessToken)
+
+    const body: User = res.body
+    expect(body.username).to.equal('cyan')
+    expect(body.account.displayName).to.equal('Cyan Garamonde')
+    expect(body.account.description).to.equal('Retainer to the king of Doma')
+    expect(body.role).to.equal(UserRole.USER)
+  })
+
+  it('Should reject token of Kefka by the plugin hook', async function () {
+    this.timeout(10000)
+
+    await wait(5000)
+
+    await getMyUserInformation(server.url, kefkaAccessToken, 401)
+  })
+
+  it('Should uninstall the plugin one and do not login Cyan', async function () {
+    await uninstallPlugin({
+      url: server.url,
+      accessToken: server.accessToken,
+      npmName: 'peertube-plugin-test-external-auth-one'
+    })
+
+    await loginExternal({
+      server,
+      npmName: 'test-external-auth-one',
+      authName: 'external-auth-1',
+      query: {
+        username: 'cyan'
+      },
+      username: 'cyan',
+      statusCodeExpected: 404
+    })
+  })
+
+  it('Should display the correct configuration', async function () {
+    const res = await getConfig(server.url)
+
+    const config: ServerConfig = res.body
+
+    const auths = config.plugin.registeredExternalAuths
+    expect(auths).to.have.lengthOf(1)
+
+    const auth2 = auths.find((a) => a.authName === 'external-auth-2')
+    expect(auth2).to.not.exist
+  })
+
+  after(async function () {
+    await cleanupTests([ server ])
+  })
+})
index c6382435decb087e495d809ef80ebba0676f92a6..6c10730aafc8f1a628f20704422492be3ac45a4d 100644 (file)
@@ -12,9 +12,9 @@ import {
   updateMyUser,
   userLogin,
   wait,
-  login, refreshToken
+  login, refreshToken, getConfig
 } from '../../../shared/extra-utils'
-import { User, UserRole } from '@shared/models'
+import { User, UserRole, ServerConfig } from '@shared/models'
 import { expect } from 'chai'
 
 describe('Test id and pass auth plugins', function () {
@@ -41,6 +41,20 @@ describe('Test id and pass auth plugins', function () {
     }
   })
 
+  it('Should display the correct configuration', async function () {
+    const res = await getConfig(server.url)
+
+    const config: ServerConfig = res.body
+
+    const auths = config.plugin.registeredIdAndPassAuths
+    expect(auths).to.have.lengthOf(8)
+
+    const crashAuth = auths.find(a => a.authName === 'crash-auth')
+    expect(crashAuth).to.exist
+    expect(crashAuth.npmName).to.equal('peertube-plugin-test-id-pass-auth-one')
+    expect(crashAuth.weight).to.equal(50)
+  })
+
   it('Should not login', async function () {
     await userLogin(server, { username: 'toto', password: 'password' }, 400)
   })
@@ -175,6 +189,18 @@ describe('Test id and pass auth plugins', function () {
     await userLogin(server, { username: 'crash', password: 'crash password' }, 400)
   })
 
+  it('Should display the correct configuration', async function () {
+    const res = await getConfig(server.url)
+
+    const config: ServerConfig = res.body
+
+    const auths = config.plugin.registeredIdAndPassAuths
+    expect(auths).to.have.lengthOf(6)
+
+    const crashAuth = auths.find(a => a.authName === 'crash-auth')
+    expect(crashAuth).to.not.exist
+  })
+
   after(async function () {
     await cleanupTests([ server ])
   })
index 8aa30654ae0d8f11b3a65ed6ac383249e4ba60b0..d2bd691312b920530d0adca60812743463bcd098 100644 (file)
@@ -1,5 +1,6 @@
 import './action-hooks'
 import './id-and-pass-auth'
+import './external-auth'
 import './filter-hooks'
 import './translations'
 import './video-constants'
index 61167f212db424cfab522c6f478d79fa5f25eb6d..0e9d67f0b0f00f0474a1354b769f7573109436fa 100644 (file)
@@ -4,6 +4,7 @@ import * as request from 'supertest'
 import { buildAbsoluteFixturePath, root } from '../miscs/miscs'
 import { isAbsolute, join } from 'path'
 import { URL } from 'url'
+import { decode } from 'querystring'
 
 function get4KFileUrl () {
   return 'https://download.cpy.re/peertube/4k_file.txt'
@@ -23,6 +24,7 @@ function makeGetRequest (options: {
   statusCodeExpected?: number
   contentType?: string
   range?: string
+  redirects?: number
 }) {
   if (!options.statusCodeExpected) options.statusCodeExpected = 400
   if (options.contentType === undefined) options.contentType = 'application/json'
@@ -33,6 +35,7 @@ function makeGetRequest (options: {
   if (options.token) req.set('Authorization', 'Bearer ' + options.token)
   if (options.query) req.query(options.query)
   if (options.range) req.set('Range', options.range)
+  if (options.redirects) req.redirects(options.redirects)
 
   return req.expect(options.statusCodeExpected)
 }
@@ -171,12 +174,17 @@ function updateAvatarRequest (options: {
   })
 }
 
+function decodeQueryString (path: string) {
+  return decode(path.split('?')[1])
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   get4KFileUrl,
   makeHTMLRequest,
   makeGetRequest,
+  decodeQueryString,
   makeUploadRequest,
   makePostBodyRequest,
   makePutBodyRequest,
index 2d02d823d5f176f1030b6e730ebefd40630243bc..b6b5e395826f648ec74016c80ba29a8a1aae380e 100644 (file)
@@ -235,6 +235,27 @@ function getPluginTestPath (suffix = '') {
   return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix)
 }
 
+function getExternalAuth (options: {
+  url: string
+  npmName: string
+  npmVersion: string
+  authName: string
+  query?: any
+  statusCodeExpected?: number
+}) {
+  const { url, npmName, npmVersion, authName, statusCodeExpected, query } = options
+
+  const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName
+
+  return makeGetRequest({
+    url,
+    path,
+    query,
+    statusCodeExpected: statusCodeExpected || 200,
+    redirects: 0
+  })
+}
+
 export {
   listPlugins,
   listAvailablePlugins,
@@ -250,5 +271,6 @@ export {
   updatePluginPackageJSON,
   getPluginPackageJSON,
   getPluginTestPath,
-  getPublicSettings
+  getPublicSettings,
+  getExternalAuth
 }
index b12b51b8c54149f7826c1a481f6f42b8fc8885f4..275bb08263e016fd83ea92c19b44166430045176 100644 (file)
@@ -95,6 +95,26 @@ function setAccessTokensToServers (servers: ServerInfo[]) {
   return Promise.all(tasks)
 }
 
+function loginUsingExternalToken (server: Server, username: string, externalAuthToken: string, expectedStatus = 200) {
+  const path = '/api/v1/users/token'
+
+  const body = {
+    client_id: server.client.id,
+    client_secret: server.client.secret,
+    username: username,
+    response_type: 'code',
+    grant_type: 'password',
+    scope: 'upload',
+    externalAuthToken
+  }
+
+  return request(server.url)
+          .post(path)
+          .type('form')
+          .send(body)
+          .expect(expectedStatus)
+}
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -107,5 +127,6 @@ export {
   setAccessTokensToServers,
   Server,
   Client,
-  User
+  User,
+  loginUsingExternalToken
 }
index 08053f01725ffbc757413a755a434acac3ca7b6d..6539dc88828cf1e2e21eef74372274a2a78f3324 100644 (file)
@@ -1,5 +1,5 @@
 import { UserRole } from '@shared/models'
-import { MOAuthToken } from '@server/typings/models'
+import { MOAuthToken, MUser } from '@server/typings/models'
 import * as express from 'express'
 
 export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions
@@ -21,7 +21,7 @@ interface RegisterServerAuthBase {
   authName: string
 
   // Called by PeerTube when a user from your plugin logged out
-  onLogout?(): void
+  onLogout?(user: MUser): void
 
   // Your plugin can hook PeerTube access/refresh token validity
   // So you can control for your plugin the user session lifetime