From: Chocobozzz Date: Wed, 29 Apr 2020 07:04:42 +0000 (+0200) Subject: Add external login tests X-Git-Tag: v2.2.0-rc.1~109 X-Git-Url: https://git.librecmc.org/?a=commitdiff_plain;h=9107d791e2eef9a1b24b0499dac8b9dbba8a792f;p=oweals%2Fpeertube.git Add external login tests --- diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts index f4be228f6..41aa26769 100644 --- a/server/controllers/api/users/token.ts +++ b/server/controllers/api/users/token.ts @@ -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 ) diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index c8623a5d4..c8e50dd53 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -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 diff --git a/server/lib/auth.ts b/server/lib/auth.ts index eaae5fdf3..2ef77bc9c 100644 --- a/server/lib/auth.ts +++ b/server/lib/auth.ts @@ -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() -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 } } diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts index 8b9975bb4..8d8a6d85e 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/oauth-model.ts @@ -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) diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index c64ca60aa..38336bcc6 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts @@ -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 ###################### diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers-store.ts index 277f2b687..151196bf1 100644 --- a/server/lib/plugins/register-helpers-store.ts +++ b/server/lib/plugins/register-helpers-store.ts @@ -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 { diff --git a/server/tests/api/check-params/plugins.ts b/server/tests/api/check-params/plugins.ts index cf80b35c2..07ded26ee 100644 --- a/server/tests/api/check-params/plugins.ts +++ b/server/tests/api/check-params/plugins.ts @@ -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 }, diff --git a/server/tests/external-plugins/auth-ldap.ts b/server/tests/external-plugins/auth-ldap.ts index 7aee986c7..0f0a08532 100644 --- a/server/tests/external-plugins/auth-ldap.ts +++ b/server/tests/external-plugins/auth-ldap.ts @@ -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 index 000000000..f29fd1f30 --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js @@ -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 index 000000000..22814b047 --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-external-auth-one/package.json @@ -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 index 000000000..34fec1bb3 --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js @@ -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 index 000000000..a5ca4d07a --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-external-auth-two/package.json @@ -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 index 000000000..a72b2829b --- /dev/null +++ b/server/tests/plugins/external-auth.ts @@ -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 ]) + }) +}) diff --git a/server/tests/plugins/id-and-pass-auth.ts b/server/tests/plugins/id-and-pass-auth.ts index c6382435d..6c10730aa 100644 --- a/server/tests/plugins/id-and-pass-auth.ts +++ b/server/tests/plugins/id-and-pass-auth.ts @@ -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 ]) }) diff --git a/server/tests/plugins/index.ts b/server/tests/plugins/index.ts index 8aa30654a..d2bd69131 100644 --- a/server/tests/plugins/index.ts +++ b/server/tests/plugins/index.ts @@ -1,5 +1,6 @@ import './action-hooks' import './id-and-pass-auth' +import './external-auth' import './filter-hooks' import './translations' import './video-constants' diff --git a/shared/extra-utils/requests/requests.ts b/shared/extra-utils/requests/requests.ts index 61167f212..0e9d67f0b 100644 --- a/shared/extra-utils/requests/requests.ts +++ b/shared/extra-utils/requests/requests.ts @@ -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, diff --git a/shared/extra-utils/server/plugins.ts b/shared/extra-utils/server/plugins.ts index 2d02d823d..b6b5e3958 100644 --- a/shared/extra-utils/server/plugins.ts +++ b/shared/extra-utils/server/plugins.ts @@ -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 } diff --git a/shared/extra-utils/users/login.ts b/shared/extra-utils/users/login.ts index b12b51b8c..275bb0826 100644 --- a/shared/extra-utils/users/login.ts +++ b/shared/extra-utils/users/login.ts @@ -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 } diff --git a/shared/models/plugins/register-server-auth.model.ts b/shared/models/plugins/register-server-auth.model.ts index 08053f017..6539dc888 100644 --- a/shared/models/plugins/register-server-auth.model.ts +++ b/shared/models/plugins/register-server-auth.model.ts @@ -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