aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/api/users/token.ts4
-rw-r--r--server/initializers/constants.ts5
-rw-r--r--server/lib/auth.ts13
-rw-r--r--server/lib/oauth-model.ts2
-rw-r--r--server/lib/plugins/plugin-manager.ts13
-rw-r--r--server/lib/plugins/register-helpers-store.ts22
-rw-r--r--server/tests/api/check-params/plugins.ts13
-rw-r--r--server/tests/external-plugins/auth-ldap.ts14
-rw-r--r--server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js67
-rw-r--r--server/tests/fixtures/peertube-plugin-test-external-auth-one/package.json20
-rw-r--r--server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js31
-rw-r--r--server/tests/fixtures/peertube-plugin-test-external-auth-two/package.json20
-rw-r--r--server/tests/plugins/external-auth.ts295
-rw-r--r--server/tests/plugins/id-and-pass-auth.ts30
-rw-r--r--server/tests/plugins/index.ts1
-rw-r--r--shared/extra-utils/requests/requests.ts8
-rw-r--r--shared/extra-utils/server/plugins.ts24
-rw-r--r--shared/extra-utils/users/login.ts23
-rw-r--r--shared/models/plugins/register-server-auth.model.ts4
19 files changed, 582 insertions, 27 deletions
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 @@
1import { handleIdAndPassLogin, handleTokenRevocation } from '@server/lib/auth' 1import { handleLogin, handleTokenRevocation } from '@server/lib/auth'
2import * as RateLimit from 'express-rate-limit' 2import * as RateLimit from 'express-rate-limit'
3import { CONFIG } from '@server/initializers/config' 3import { CONFIG } from '@server/initializers/config'
4import * as express from 'express' 4import * as express from 'express'
@@ -14,7 +14,7 @@ const loginRateLimiter = RateLimit({
14 14
15tokensRouter.post('/token', 15tokensRouter.post('/token',
16 loginRateLimiter, 16 loginRateLimiter,
17 handleIdAndPassLogin, 17 handleLogin,
18 tokenSuccess 18 tokenSuccess
19) 19)
20 20
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
645const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css' 645const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css'
646const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME) 646const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME)
647 647
648let PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 1000 * 60 * 5 // 5 minutes
649
648const DEFAULT_THEME_NAME = 'default' 650const DEFAULT_THEME_NAME = 'default'
649const DEFAULT_USER_THEME_NAME = 'instance-default' 651const DEFAULT_USER_THEME_NAME = 'instance-default'
650 652
@@ -686,6 +688,8 @@ if (isTestInstance() === true) {
686 FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 688 FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000
687 MEMOIZE_TTL.OVERVIEWS_SAMPLE = 3000 689 MEMOIZE_TTL.OVERVIEWS_SAMPLE = 3000
688 OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD = 2 690 OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD = 2
691
692 PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000
689} 693}
690 694
691updateWebserverUrls() 695updateWebserverUrls()
@@ -778,6 +782,7 @@ export {
778 VIDEO_VIEW_LIFETIME, 782 VIDEO_VIEW_LIFETIME,
779 CONTACT_FORM_LIFETIME, 783 CONTACT_FORM_LIFETIME,
780 VIDEO_PLAYLIST_PRIVACIES, 784 VIDEO_PLAYLIST_PRIVACIES,
785 PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME,
781 ASSETS_PATH, 786 ASSETS_PATH,
782 loadLanguages, 787 loadLanguages,
783 buildLanguages 788 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 @@
1import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' 1import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
2import { logger } from '@server/helpers/logger' 2import { logger } from '@server/helpers/logger'
3import { generateRandomString } from '@server/helpers/utils' 3import { generateRandomString } from '@server/helpers/utils'
4import { OAUTH_LIFETIME, WEBSERVER } from '@server/initializers/constants' 4import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants'
5import { revokeToken } from '@server/lib/oauth-model' 5import { revokeToken } from '@server/lib/oauth-model'
6import { PluginManager } from '@server/lib/plugins/plugin-manager' 6import { PluginManager } from '@server/lib/plugins/plugin-manager'
7import { OAuthTokenModel } from '@server/models/oauth/oauth-token' 7import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
@@ -35,7 +35,7 @@ const authBypassTokens = new Map<string, {
35 npmName: string 35 npmName: string
36}>() 36}>()
37 37
38async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) { 38async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
39 const grantType = req.body.grant_type 39 const grantType = req.body.grant_type
40 40
41 if (grantType === 'password') { 41 if (grantType === 'password') {
@@ -90,10 +90,9 @@ async function onExternalUserAuthenticated (options: {
90 logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName) 90 logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName)
91 91
92 const bypassToken = await generateRandomString(32) 92 const bypassToken = await generateRandomString(32)
93 const tokenLifetime = 1000 * 60 * 5 // 5 minutes
94 93
95 const expires = new Date() 94 const expires = new Date()
96 expires.setTime(expires.getTime() + tokenLifetime) 95 expires.setTime(expires.getTime() + PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME)
97 96
98 const user = buildUserResult(authResult) 97 const user = buildUserResult(authResult)
99 authBypassTokens.set(bypassToken, { 98 authBypassTokens.set(bypassToken, {
@@ -108,7 +107,7 @@ async function onExternalUserAuthenticated (options: {
108 107
109// --------------------------------------------------------------------------- 108// ---------------------------------------------------------------------------
110 109
111export { oAuthServer, handleIdAndPassLogin, onExternalUserAuthenticated, handleTokenRevocation } 110export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation }
112 111
113// --------------------------------------------------------------------------- 112// ---------------------------------------------------------------------------
114 113
@@ -212,7 +211,7 @@ function proxifyExternalAuthBypass (req: express.Request, res: express.Response)
212 211
213 const now = new Date() 212 const now = new Date()
214 if (now.getTime() > expires.getTime()) { 213 if (now.getTime() > expires.getTime()) {
215 logger.error('Cannot authenticate user with an expired bypass token') 214 logger.error('Cannot authenticate user with an expired external auth token')
216 return res.sendStatus(400) 215 return res.sendStatus(400)
217 } 216 }
218 217
@@ -267,7 +266,7 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
267 return { 266 return {
268 username: pluginResult.username, 267 username: pluginResult.username,
269 email: pluginResult.email, 268 email: pluginResult.email,
270 role: pluginResult.role || UserRole.USER, 269 role: pluginResult.role ?? UserRole.USER,
271 displayName: pluginResult.displayName || pluginResult.username 270 displayName: pluginResult.displayName || pluginResult.username
272 } 271 }
273} 272}
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 }) {
139 139
140 if (token) { 140 if (token) {
141 if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) { 141 if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) {
142 PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName) 142 PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User)
143 } 143 }
144 144
145 clearCacheByToken(token.accessToken) 145 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'
21import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' 21import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model'
22import { RegisterHelpersStore } from './register-helpers-store' 22import { RegisterHelpersStore } from './register-helpers-store'
23import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' 23import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
24import { MOAuthTokenUser } from '@server/typings/models' 24import { MOAuthTokenUser, MUser } from '@server/typings/models'
25import { RegisterServerAuthPassOptions, RegisterServerAuthExternalOptions } from '@shared/models/plugins/register-server-auth.model'
25 26
26export interface RegisteredPlugin { 27export interface RegisteredPlugin {
27 npmName: string 28 npmName: string
@@ -133,14 +134,14 @@ export class PluginManager implements ServerHook {
133 return this.translations[locale] || {} 134 return this.translations[locale] || {}
134 } 135 }
135 136
136 onLogout (npmName: string, authName: string) { 137 onLogout (npmName: string, authName: string, user: MUser) {
137 const auth = this.getAuth(npmName, authName) 138 const auth = this.getAuth(npmName, authName)
138 139
139 if (auth?.onLogout) { 140 if (auth?.onLogout) {
140 logger.info('Running onLogout function from auth %s of plugin %s', authName, npmName) 141 logger.info('Running onLogout function from auth %s of plugin %s', authName, npmName)
141 142
142 try { 143 try {
143 auth.onLogout() 144 auth.onLogout(user)
144 } catch (err) { 145 } catch (err) {
145 logger.warn('Cannot run onLogout function from auth %s of plugin %s.', authName, npmName, { err }) 146 logger.warn('Cannot run onLogout function from auth %s of plugin %s.', authName, npmName, { err })
146 } 147 }
@@ -478,8 +479,10 @@ export class PluginManager implements ServerHook {
478 const plugin = this.getRegisteredPluginOrTheme(npmName) 479 const plugin = this.getRegisteredPluginOrTheme(npmName)
479 if (!plugin || plugin.type !== PluginType.PLUGIN) return null 480 if (!plugin || plugin.type !== PluginType.PLUGIN) return null
480 481
481 return plugin.registerHelpersStore.getIdAndPassAuths() 482 let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpersStore.getIdAndPassAuths()
482 .find(a => a.authName === authName) 483 auths = auths.concat(plugin.registerHelpersStore.getExternalAuths())
484
485 return auths.find(a => a.authName === authName)
483 } 486 }
484 487
485 // ###################### Private getters ###################### 488 // ###################### 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 @@
1import * as express from 'express'
1import { logger } from '@server/helpers/logger' 2import { logger } from '@server/helpers/logger'
2import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PRIVACIES } from '@server/initializers/constants' 3import {
4 VIDEO_CATEGORIES,
5 VIDEO_LANGUAGES,
6 VIDEO_LICENCES,
7 VIDEO_PLAYLIST_PRIVACIES,
8 VIDEO_PRIVACIES
9} from '@server/initializers/constants'
3import { onExternalUserAuthenticated } from '@server/lib/auth' 10import { onExternalUserAuthenticated } from '@server/lib/auth'
4import { PluginModel } from '@server/models/server/plugin' 11import { PluginModel } from '@server/models/server/plugin'
5import { RegisterServerOptions } from '@server/typings/plugins' 12import { RegisterServerOptions } from '@server/typings/plugins'
@@ -10,11 +17,15 @@ import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video-
10import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model' 17import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model'
11import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model' 18import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model'
12import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model' 19import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model'
13import { RegisterServerAuthExternalOptions, RegisterServerAuthExternalResult, RegisterServerAuthPassOptions, RegisterServerExternalAuthenticatedResult } from '@shared/models/plugins/register-server-auth.model' 20import {
21 RegisterServerAuthExternalOptions,
22 RegisterServerAuthExternalResult,
23 RegisterServerAuthPassOptions,
24 RegisterServerExternalAuthenticatedResult
25} from '@shared/models/plugins/register-server-auth.model'
14import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' 26import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
15import { RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model' 27import { RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model'
16import { serverHookObject } from '@shared/models/plugins/server-hook.model' 28import { serverHookObject } from '@shared/models/plugins/server-hook.model'
17import * as express from 'express'
18import { buildPluginHelpers } from './plugin-helpers' 29import { buildPluginHelpers } from './plugin-helpers'
19 30
20type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' 31type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
@@ -174,6 +185,11 @@ export class RegisterHelpersStore {
174 const self = this 185 const self = this
175 186
176 return (options: RegisterServerAuthExternalOptions) => { 187 return (options: RegisterServerAuthExternalOptions) => {
188 if (!options.authName || !options.onAuthRequest || typeof options.onAuthRequest !== 'function') {
189 logger.error('Cannot register auth plugin %s: authName of getWeight or login are not valid.', this.npmName)
190 return
191 }
192
177 this.externalAuths.push(options) 193 this.externalAuths.push(options)
178 194
179 return { 195 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 () {
64 describe('With static plugin routes', function () { 64 describe('With static plugin routes', function () {
65 it('Should fail with an unknown plugin name/plugin version', async function () { 65 it('Should fail with an unknown plugin name/plugin version', async function () {
66 const paths = [ 66 const paths = [
67 '/plugins/' + pluginName + '/0.0.1/auth/fake-auth',
67 '/plugins/' + pluginName + '/0.0.1/static/images/chocobo.png', 68 '/plugins/' + pluginName + '/0.0.1/static/images/chocobo.png',
68 '/plugins/' + pluginName + '/0.0.1/client-scripts/client/common-client-plugin.js', 69 '/plugins/' + pluginName + '/0.0.1/client-scripts/client/common-client-plugin.js',
69 '/themes/' + themeName + '/0.0.1/static/images/chocobo.png', 70 '/themes/' + themeName + '/0.0.1/static/images/chocobo.png',
@@ -86,6 +87,7 @@ describe('Test server plugins API validators', function () {
86 87
87 it('Should fail with invalid versions', async function () { 88 it('Should fail with invalid versions', async function () {
88 const paths = [ 89 const paths = [
90 '/plugins/' + pluginName + '/0.0.1.1/auth/fake-auth',
89 '/plugins/' + pluginName + '/0.0.1.1/static/images/chocobo.png', 91 '/plugins/' + pluginName + '/0.0.1.1/static/images/chocobo.png',
90 '/plugins/' + pluginName + '/0.1/client-scripts/client/common-client-plugin.js', 92 '/plugins/' + pluginName + '/0.1/client-scripts/client/common-client-plugin.js',
91 '/themes/' + themeName + '/1/static/images/chocobo.png', 93 '/themes/' + themeName + '/1/static/images/chocobo.png',
@@ -112,6 +114,12 @@ describe('Test server plugins API validators', function () {
112 } 114 }
113 }) 115 })
114 116
117 it('Should fail with an unknown auth name', async function () {
118 const path = '/plugins/' + pluginName + '/' + npmVersion + '/auth/bad-auth'
119
120 await makeGetRequest({ url: server.url, path, statusCodeExpected: 404 })
121 })
122
115 it('Should fail with an unknown static file', async function () { 123 it('Should fail with an unknown static file', async function () {
116 const paths = [ 124 const paths = [
117 '/plugins/' + pluginName + '/' + npmVersion + '/static/fake/chocobo.png', 125 '/plugins/' + pluginName + '/' + npmVersion + '/static/fake/chocobo.png',
@@ -145,6 +153,9 @@ describe('Test server plugins API validators', function () {
145 for (const p of paths) { 153 for (const p of paths) {
146 await makeGetRequest({ url: server.url, path: p, statusCodeExpected: 200 }) 154 await makeGetRequest({ url: server.url, path: p, statusCodeExpected: 200 })
147 } 155 }
156
157 const authPath = '/plugins/' + pluginName + '/' + npmVersion + '/auth/fake-auth'
158 await makeGetRequest({ url: server.url, path: authPath, statusCodeExpected: 302 })
148 }) 159 })
149 }) 160 })
150 161
@@ -462,6 +473,8 @@ describe('Test server plugins API validators', function () {
462 }) 473 })
463 474
464 it('Should succeed with the correct parameters', async function () { 475 it('Should succeed with the correct parameters', async function () {
476 this.timeout(10000)
477
465 const it = [ 478 const it = [
466 { suffix: 'install', status: 200 }, 479 { suffix: 'install', status: 200 },
467 { suffix: 'update', status: 200 }, 480 { 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 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import { getMyUserInformation, installPlugin, setAccessTokensToServers, updatePluginSettings, userLogin, uploadVideo, uninstallPlugin } from '../../../shared/extra-utils'
5import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
6import { User } from '@shared/models/users/user.model'
7import { expect } from 'chai' 4import { expect } from 'chai'
5import { User } from '@shared/models/users/user.model'
6import {
7 getMyUserInformation,
8 installPlugin,
9 setAccessTokensToServers,
10 uninstallPlugin,
11 updatePluginSettings,
12 uploadVideo,
13 userLogin
14} from '../../../shared/extra-utils'
15import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
8 16
9describe('Official plugin auth-ldap', function () { 17describe('Official plugin auth-ldap', function () {
10 let server: ServerInfo 18 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 @@
1async function register ({
2 registerExternalAuth,
3 peertubeHelpers
4}) {
5 {
6 const result = registerExternalAuth({
7 authName: 'external-auth-1',
8 authDisplayName: 'External Auth 1',
9 onLogout: user => peertubeHelpers.logger.info('On logout %s', user.username),
10 onAuthRequest: (req, res) => {
11 const username = req.query.username
12
13 result.userAuthenticated({
14 req,
15 res,
16 username,
17 email: username + '@example.com'
18 })
19 }
20 })
21 }
22
23 {
24 const result = registerExternalAuth({
25 authName: 'external-auth-2',
26 authDisplayName: 'External Auth 2',
27 onAuthRequest: (req, res) => {
28 result.userAuthenticated({
29 req,
30 res,
31 username: 'kefka',
32 email: 'kefka@example.com',
33 role: 0,
34 displayName: 'Kefka Palazzo'
35 })
36 },
37 hookTokenValidity: (options) => {
38 if (options.type === 'refresh') {
39 return { valid: false }
40 }
41
42 if (options.type === 'access') {
43 const token = options.token
44 const now = new Date()
45 now.setTime(now.getTime() - 5000)
46
47 const createdAt = new Date(token.createdAt)
48
49 return { valid: createdAt.getTime() >= now.getTime() }
50 }
51
52 return { valid: true }
53 }
54 })
55 }
56}
57
58async function unregister () {
59 return
60}
61
62module.exports = {
63 register,
64 unregister
65}
66
67// ###########################################################################
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 @@
1{
2 "name": "peertube-plugin-test-external-auth-one",
3 "version": "0.0.1",
4 "description": "External auth one",
5 "engine": {
6 "peertube": ">=1.3.0"
7 },
8 "keywords": [
9 "peertube",
10 "plugin"
11 ],
12 "homepage": "https://github.com/Chocobozzz/PeerTube",
13 "author": "Chocobozzz",
14 "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
15 "library": "./main.js",
16 "staticDirs": {},
17 "css": [],
18 "clientScripts": [],
19 "translations": {}
20}
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 @@
1async function register ({
2 registerExternalAuth,
3 peertubeHelpers
4}) {
5 {
6 const result = registerExternalAuth({
7 authName: 'external-auth-3',
8 authDisplayName: 'External Auth 3',
9 onAuthRequest: (req, res) => {
10 result.userAuthenticated({
11 req,
12 res,
13 username: 'cid',
14 email: 'cid@example.com',
15 displayName: 'Cid Marquez'
16 })
17 }
18 })
19 }
20}
21
22async function unregister () {
23 return
24}
25
26module.exports = {
27 register,
28 unregister
29}
30
31// ###########################################################################
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 @@
1{
2 "name": "peertube-plugin-test-external-auth-two",
3 "version": "0.0.1",
4 "description": "External auth two",
5 "engine": {
6 "peertube": ">=1.3.0"
7 },
8 "keywords": [
9 "peertube",
10 "plugin"
11 ],
12 "homepage": "https://github.com/Chocobozzz/PeerTube",
13 "author": "Chocobozzz",
14 "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
15 "library": "./main.js",
16 "staticDirs": {},
17 "css": [],
18 "clientScripts": [],
19 "translations": {}
20}
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 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { expect } from 'chai'
5import { ServerConfig, User, UserRole } from '@shared/models'
6import {
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} from '../../../shared/extra-utils'
21import { cleanupTests, flushAndRunServer, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
22
23async function loginExternal (options: {
24 server: ServerInfo
25 npmName: string
26 authName: string
27 username: string
28 query?: any
29 statusCodeExpected?: number
30}) {
31 const res = await getExternalAuth({
32 url: options.server.url,
33 npmName: options.npmName,
34 npmVersion: '0.0.1',
35 authName: options.authName,
36 query: options.query,
37 statusCodeExpected: options.statusCodeExpected || 302
38 })
39
40 if (res.status !== 302) return
41
42 const location = res.header.location
43 const { externalAuthToken } = decodeQueryString(location)
44
45 const resLogin = await loginUsingExternalToken(
46 options.server,
47 options.username,
48 externalAuthToken as string
49 )
50
51 return resLogin.body
52}
53
54describe('Test external auth plugins', function () {
55 let server: ServerInfo
56
57 let cyanAccessToken: string
58 let cyanRefreshToken: string
59
60 let kefkaAccessToken: string
61 let kefkaRefreshToken: string
62
63 let externalAuthToken: string
64
65 before(async function () {
66 this.timeout(30000)
67
68 server = await flushAndRunServer(1)
69 await setAccessTokensToServers([ server ])
70
71 for (const suffix of [ 'one', 'two' ]) {
72 await installPlugin({
73 url: server.url,
74 accessToken: server.accessToken,
75 path: getPluginTestPath('-external-auth-' + suffix)
76 })
77 }
78 })
79
80 it('Should display the correct configuration', async function () {
81 const res = await getConfig(server.url)
82
83 const config: ServerConfig = res.body
84
85 const auths = config.plugin.registeredExternalAuths
86 expect(auths).to.have.lengthOf(3)
87
88 const auth2 = auths.find((a) => a.authName === 'external-auth-2')
89 expect(auth2).to.exist
90 expect(auth2.authDisplayName).to.equal('External Auth 2')
91 expect(auth2.npmName).to.equal('peertube-plugin-test-external-auth-one')
92 })
93
94 it('Should redirect for a Cyan login', async function () {
95 const res = await getExternalAuth({
96 url: server.url,
97 npmName: 'test-external-auth-one',
98 npmVersion: '0.0.1',
99 authName: 'external-auth-1',
100 query: {
101 username: 'cyan'
102 },
103 statusCodeExpected: 302
104 })
105
106 const location = res.header.location
107 expect(location.startsWith('/login?')).to.be.true
108
109 const searchParams = decodeQueryString(location)
110
111 expect(searchParams.externalAuthToken).to.exist
112 expect(searchParams.username).to.equal('cyan')
113
114 externalAuthToken = searchParams.externalAuthToken as string
115 })
116
117 it('Should reject auto external login with a missing or invalid token', async function () {
118 await loginUsingExternalToken(server, 'cyan', '', 400)
119 await loginUsingExternalToken(server, 'cyan', 'blabla', 400)
120 })
121
122 it('Should reject auto external login with a missing or invalid username', async function () {
123 await loginUsingExternalToken(server, '', externalAuthToken, 400)
124 await loginUsingExternalToken(server, '', externalAuthToken, 400)
125 })
126
127 it('Should reject auto external login with an expired token', async function () {
128 this.timeout(15000)
129
130 await wait(5000)
131
132 await loginUsingExternalToken(server, 'cyan', externalAuthToken, 400)
133
134 await waitUntilLog(server, 'expired external auth token')
135 })
136
137 it('Should auto login Cyan, create the user and use the token', async function () {
138 {
139 const res = await loginExternal({
140 server,
141 npmName: 'test-external-auth-one',
142 authName: 'external-auth-1',
143 query: {
144 username: 'cyan'
145 },
146 username: 'cyan'
147 })
148
149 cyanAccessToken = res.access_token
150 cyanRefreshToken = res.refresh_token
151 }
152
153 {
154 const res = await getMyUserInformation(server.url, cyanAccessToken)
155
156 const body: User = res.body
157 expect(body.username).to.equal('cyan')
158 expect(body.account.displayName).to.equal('cyan')
159 expect(body.email).to.equal('cyan@example.com')
160 expect(body.role).to.equal(UserRole.USER)
161 }
162 })
163
164 it('Should auto login Kefka, create the user and use the token', async function () {
165 {
166 const res = await loginExternal({
167 server,
168 npmName: 'test-external-auth-one',
169 authName: 'external-auth-2',
170 username: 'kefka'
171 })
172
173 kefkaAccessToken = res.access_token
174 kefkaRefreshToken = res.refresh_token
175 }
176
177 {
178 const res = await getMyUserInformation(server.url, kefkaAccessToken)
179
180 const body: User = res.body
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).to.equal(UserRole.ADMINISTRATOR)
185 }
186 })
187
188 it('Should refresh Cyan token, but not Kefka token', async function () {
189 {
190 const resRefresh = await refreshToken(server, cyanRefreshToken)
191 cyanAccessToken = resRefresh.body.access_token
192 cyanRefreshToken = resRefresh.body.refresh_token
193
194 const res = await getMyUserInformation(server.url, cyanAccessToken)
195 const user: User = res.body
196 expect(user.username).to.equal('cyan')
197 }
198
199 {
200 await refreshToken(server, kefkaRefreshToken, 400)
201 }
202 })
203
204 it('Should update Cyan profile', async function () {
205 await updateMyUser({
206 url: server.url,
207 accessToken: cyanAccessToken,
208 displayName: 'Cyan Garamonde',
209 description: 'Retainer to the king of Doma'
210 })
211
212 const res = await getMyUserInformation(server.url, cyanAccessToken)
213
214 const body: User = res.body
215 expect(body.account.displayName).to.equal('Cyan Garamonde')
216 expect(body.account.description).to.equal('Retainer to the king of Doma')
217 })
218
219 it('Should logout Cyan', async function () {
220 await logout(server.url, cyanAccessToken)
221 })
222
223 it('Should have logged out Cyan', async function () {
224 await waitUntilLog(server, 'On logout cyan')
225
226 await getMyUserInformation(server.url, cyanAccessToken, 401)
227 })
228
229 it('Should login Cyan and keep the old existing profile', async function () {
230 {
231 const res = await loginExternal({
232 server,
233 npmName: 'test-external-auth-one',
234 authName: 'external-auth-1',
235 query: {
236 username: 'cyan'
237 },
238 username: 'cyan'
239 })
240
241 cyanAccessToken = res.access_token
242 }
243
244 const res = await getMyUserInformation(server.url, cyanAccessToken)
245
246 const body: User = res.body
247 expect(body.username).to.equal('cyan')
248 expect(body.account.displayName).to.equal('Cyan Garamonde')
249 expect(body.account.description).to.equal('Retainer to the king of Doma')
250 expect(body.role).to.equal(UserRole.USER)
251 })
252
253 it('Should reject token of Kefka by the plugin hook', async function () {
254 this.timeout(10000)
255
256 await wait(5000)
257
258 await getMyUserInformation(server.url, kefkaAccessToken, 401)
259 })
260
261 it('Should uninstall the plugin one and do not login Cyan', async function () {
262 await uninstallPlugin({
263 url: server.url,
264 accessToken: server.accessToken,
265 npmName: 'peertube-plugin-test-external-auth-one'
266 })
267
268 await loginExternal({
269 server,
270 npmName: 'test-external-auth-one',
271 authName: 'external-auth-1',
272 query: {
273 username: 'cyan'
274 },
275 username: 'cyan',
276 statusCodeExpected: 404
277 })
278 })
279
280 it('Should display the correct configuration', async function () {
281 const res = await getConfig(server.url)
282
283 const config: ServerConfig = res.body
284
285 const auths = config.plugin.registeredExternalAuths
286 expect(auths).to.have.lengthOf(1)
287
288 const auth2 = auths.find((a) => a.authName === 'external-auth-2')
289 expect(auth2).to.not.exist
290 })
291
292 after(async function () {
293 await cleanupTests([ server ])
294 })
295})
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 {
12 updateMyUser, 12 updateMyUser,
13 userLogin, 13 userLogin,
14 wait, 14 wait,
15 login, refreshToken 15 login, refreshToken, getConfig
16} from '../../../shared/extra-utils' 16} from '../../../shared/extra-utils'
17import { User, UserRole } from '@shared/models' 17import { User, UserRole, ServerConfig } from '@shared/models'
18import { expect } from 'chai' 18import { expect } from 'chai'
19 19
20describe('Test id and pass auth plugins', function () { 20describe('Test id and pass auth plugins', function () {
@@ -41,6 +41,20 @@ describe('Test id and pass auth plugins', function () {
41 } 41 }
42 }) 42 })
43 43
44 it('Should display the correct configuration', async function () {
45 const res = await getConfig(server.url)
46
47 const config: ServerConfig = res.body
48
49 const auths = config.plugin.registeredIdAndPassAuths
50 expect(auths).to.have.lengthOf(8)
51
52 const crashAuth = auths.find(a => a.authName === 'crash-auth')
53 expect(crashAuth).to.exist
54 expect(crashAuth.npmName).to.equal('peertube-plugin-test-id-pass-auth-one')
55 expect(crashAuth.weight).to.equal(50)
56 })
57
44 it('Should not login', async function () { 58 it('Should not login', async function () {
45 await userLogin(server, { username: 'toto', password: 'password' }, 400) 59 await userLogin(server, { username: 'toto', password: 'password' }, 400)
46 }) 60 })
@@ -175,6 +189,18 @@ describe('Test id and pass auth plugins', function () {
175 await userLogin(server, { username: 'crash', password: 'crash password' }, 400) 189 await userLogin(server, { username: 'crash', password: 'crash password' }, 400)
176 }) 190 })
177 191
192 it('Should display the correct configuration', async function () {
193 const res = await getConfig(server.url)
194
195 const config: ServerConfig = res.body
196
197 const auths = config.plugin.registeredIdAndPassAuths
198 expect(auths).to.have.lengthOf(6)
199
200 const crashAuth = auths.find(a => a.authName === 'crash-auth')
201 expect(crashAuth).to.not.exist
202 })
203
178 after(async function () { 204 after(async function () {
179 await cleanupTests([ server ]) 205 await cleanupTests([ server ])
180 }) 206 })
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 @@
1import './action-hooks' 1import './action-hooks'
2import './id-and-pass-auth' 2import './id-and-pass-auth'
3import './external-auth'
3import './filter-hooks' 4import './filter-hooks'
4import './translations' 5import './translations'
5import './video-constants' 6import './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'
4import { buildAbsoluteFixturePath, root } from '../miscs/miscs' 4import { buildAbsoluteFixturePath, root } from '../miscs/miscs'
5import { isAbsolute, join } from 'path' 5import { isAbsolute, join } from 'path'
6import { URL } from 'url' 6import { URL } from 'url'
7import { decode } from 'querystring'
7 8
8function get4KFileUrl () { 9function get4KFileUrl () {
9 return 'https://download.cpy.re/peertube/4k_file.txt' 10 return 'https://download.cpy.re/peertube/4k_file.txt'
@@ -23,6 +24,7 @@ function makeGetRequest (options: {
23 statusCodeExpected?: number 24 statusCodeExpected?: number
24 contentType?: string 25 contentType?: string
25 range?: string 26 range?: string
27 redirects?: number
26}) { 28}) {
27 if (!options.statusCodeExpected) options.statusCodeExpected = 400 29 if (!options.statusCodeExpected) options.statusCodeExpected = 400
28 if (options.contentType === undefined) options.contentType = 'application/json' 30 if (options.contentType === undefined) options.contentType = 'application/json'
@@ -33,6 +35,7 @@ function makeGetRequest (options: {
33 if (options.token) req.set('Authorization', 'Bearer ' + options.token) 35 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
34 if (options.query) req.query(options.query) 36 if (options.query) req.query(options.query)
35 if (options.range) req.set('Range', options.range) 37 if (options.range) req.set('Range', options.range)
38 if (options.redirects) req.redirects(options.redirects)
36 39
37 return req.expect(options.statusCodeExpected) 40 return req.expect(options.statusCodeExpected)
38} 41}
@@ -171,12 +174,17 @@ function updateAvatarRequest (options: {
171 }) 174 })
172} 175}
173 176
177function decodeQueryString (path: string) {
178 return decode(path.split('?')[1])
179}
180
174// --------------------------------------------------------------------------- 181// ---------------------------------------------------------------------------
175 182
176export { 183export {
177 get4KFileUrl, 184 get4KFileUrl,
178 makeHTMLRequest, 185 makeHTMLRequest,
179 makeGetRequest, 186 makeGetRequest,
187 decodeQueryString,
180 makeUploadRequest, 188 makeUploadRequest,
181 makePostBodyRequest, 189 makePostBodyRequest,
182 makePutBodyRequest, 190 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 = '') {
235 return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix) 235 return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix)
236} 236}
237 237
238function getExternalAuth (options: {
239 url: string
240 npmName: string
241 npmVersion: string
242 authName: string
243 query?: any
244 statusCodeExpected?: number
245}) {
246 const { url, npmName, npmVersion, authName, statusCodeExpected, query } = options
247
248 const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName
249
250 return makeGetRequest({
251 url,
252 path,
253 query,
254 statusCodeExpected: statusCodeExpected || 200,
255 redirects: 0
256 })
257}
258
238export { 259export {
239 listPlugins, 260 listPlugins,
240 listAvailablePlugins, 261 listAvailablePlugins,
@@ -250,5 +271,6 @@ export {
250 updatePluginPackageJSON, 271 updatePluginPackageJSON,
251 getPluginPackageJSON, 272 getPluginPackageJSON,
252 getPluginTestPath, 273 getPluginTestPath,
253 getPublicSettings 274 getPublicSettings,
275 getExternalAuth
254} 276}
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[]) {
95 return Promise.all(tasks) 95 return Promise.all(tasks)
96} 96}
97 97
98function loginUsingExternalToken (server: Server, username: string, externalAuthToken: string, expectedStatus = 200) {
99 const path = '/api/v1/users/token'
100
101 const body = {
102 client_id: server.client.id,
103 client_secret: server.client.secret,
104 username: username,
105 response_type: 'code',
106 grant_type: 'password',
107 scope: 'upload',
108 externalAuthToken
109 }
110
111 return request(server.url)
112 .post(path)
113 .type('form')
114 .send(body)
115 .expect(expectedStatus)
116}
117
98// --------------------------------------------------------------------------- 118// ---------------------------------------------------------------------------
99 119
100export { 120export {
@@ -107,5 +127,6 @@ export {
107 setAccessTokensToServers, 127 setAccessTokensToServers,
108 Server, 128 Server,
109 Client, 129 Client,
110 User 130 User,
131 loginUsingExternalToken
111} 132}
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 @@
1import { UserRole } from '@shared/models' 1import { UserRole } from '@shared/models'
2import { MOAuthToken } from '@server/typings/models' 2import { MOAuthToken, MUser } from '@server/typings/models'
3import * as express from 'express' 3import * as express from 'express'
4 4
5export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions 5export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions
@@ -21,7 +21,7 @@ interface RegisterServerAuthBase {
21 authName: string 21 authName: string
22 22
23 // Called by PeerTube when a user from your plugin logged out 23 // Called by PeerTube when a user from your plugin logged out
24 onLogout?(): void 24 onLogout?(user: MUser): void
25 25
26 // Your plugin can hook PeerTube access/refresh token validity 26 // Your plugin can hook PeerTube access/refresh token validity
27 // So you can control for your plugin the user session lifetime 27 // So you can control for your plugin the user session lifetime