aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-04-22 16:07:04 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-05-04 16:21:39 +0200
commit7fed637506043e4432cbebe041ada0625171cceb (patch)
tree07f174e17c4b4a0b3d43a0fa6944865c06234338 /server
parent8d4197637868d5cde49434e937186b57e40f4b2b (diff)
downloadPeerTube-7fed637506043e4432cbebe041ada0625171cceb.tar.gz
PeerTube-7fed637506043e4432cbebe041ada0625171cceb.tar.zst
PeerTube-7fed637506043e4432cbebe041ada0625171cceb.zip
Begin auth plugin support
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/users/index.ts14
-rw-r--r--server/lib/auth.ts101
-rw-r--r--server/lib/oauth-model.ts76
-rw-r--r--server/lib/plugins/plugin-manager.ts55
-rw-r--r--server/lib/plugins/register-helpers-store.ts43
-rw-r--r--server/lib/user.ts3
-rw-r--r--server/lib/video-channel.ts4
-rw-r--r--server/middlewares/oauth.ts29
-rw-r--r--server/models/account/user.ts7
-rw-r--r--server/tests/api/users/users.ts14
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/main.js61
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/package.json20
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/main.js37
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/package.json20
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js36
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/package.json20
-rw-r--r--server/tests/plugins/id-and-pass-auth.ts69
-rw-r--r--server/tests/plugins/index.ts1
-rw-r--r--server/typings/express.ts13
-rw-r--r--server/typings/plugins/register-server-option.model.ts4
20 files changed, 553 insertions, 74 deletions
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 98eb2beed..b30f42b43 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -17,7 +17,6 @@ import {
17 paginationValidator, 17 paginationValidator,
18 setDefaultPagination, 18 setDefaultPagination,
19 setDefaultSort, 19 setDefaultSort,
20 token,
21 userAutocompleteValidator, 20 userAutocompleteValidator,
22 usersAddValidator, 21 usersAddValidator,
23 usersGetValidator, 22 usersGetValidator,
@@ -50,6 +49,7 @@ import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
50import { UserRegister } from '../../../../shared/models/users/user-register.model' 49import { UserRegister } from '../../../../shared/models/users/user-register.model'
51import { MUser, MUserAccountDefault } from '@server/typings/models' 50import { MUser, MUserAccountDefault } from '@server/typings/models'
52import { Hooks } from '@server/lib/plugins/hooks' 51import { Hooks } from '@server/lib/plugins/hooks'
52import { handleIdAndPassLogin } from '@server/lib/auth'
53 53
54const auditLogger = auditLoggerFactory('users') 54const auditLogger = auditLoggerFactory('users')
55 55
@@ -170,7 +170,17 @@ usersRouter.post('/:id/verify-email',
170 170
171usersRouter.post('/token', 171usersRouter.post('/token',
172 loginRateLimiter, 172 loginRateLimiter,
173 token, 173 handleIdAndPassLogin,
174 tokenSuccess
175)
176usersRouter.post('/token',
177 loginRateLimiter,
178 handleIdAndPassLogin,
179 tokenSuccess
180)
181usersRouter.post('/revoke-token',
182 loginRateLimiter,
183 handleIdAndPassLogin,
174 tokenSuccess 184 tokenSuccess
175) 185)
176// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route 186// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route
diff --git a/server/lib/auth.ts b/server/lib/auth.ts
new file mode 100644
index 000000000..18d52fa5a
--- /dev/null
+++ b/server/lib/auth.ts
@@ -0,0 +1,101 @@
1import * as express from 'express'
2import { OAUTH_LIFETIME } from '@server/initializers/constants'
3import * as OAuthServer from 'express-oauth-server'
4import { PluginManager } from '@server/lib/plugins/plugin-manager'
5import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-server-auth.model'
6import { logger } from '@server/helpers/logger'
7import { UserRole } from '@shared/models'
8
9const oAuthServer = new OAuthServer({
10 useErrorHandler: true,
11 accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
12 refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
13 continueMiddleware: true,
14 model: require('./oauth-model')
15})
16
17function onExternalAuthPlugin (npmName: string, username: string, email: string) {
18
19}
20
21async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
22 const plugins = PluginManager.Instance.getIdAndPassAuths()
23 const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = []
24
25 for (const plugin of plugins) {
26 const auths = plugin.idAndPassAuths
27
28 for (const auth of auths) {
29 pluginAuths.push({
30 npmName: plugin.npmName,
31 registerAuthOptions: auth
32 })
33 }
34 }
35
36 pluginAuths.sort((a, b) => {
37 const aWeight = a.registerAuthOptions.getWeight()
38 const bWeight = b.registerAuthOptions.getWeight()
39
40 if (aWeight === bWeight) return 0
41 if (aWeight > bWeight) return 1
42 return -1
43 })
44
45 const loginOptions = {
46 id: req.body.username,
47 password: req.body.password
48 }
49
50 for (const pluginAuth of pluginAuths) {
51 logger.debug(
52 'Using auth method of %s to login %s with weight %d.',
53 pluginAuth.npmName, loginOptions.id, pluginAuth.registerAuthOptions.getWeight()
54 )
55
56 const loginResult = await pluginAuth.registerAuthOptions.login(loginOptions)
57 if (loginResult) {
58 logger.info('Login success with plugin %s for %s.', pluginAuth.npmName, loginOptions.id)
59
60 res.locals.bypassLogin = {
61 bypass: true,
62 pluginName: pluginAuth.npmName,
63 user: {
64 username: loginResult.username,
65 email: loginResult.email,
66 role: loginResult.role || UserRole.USER,
67 displayName: loginResult.displayName || loginResult.username
68 }
69 }
70
71 break
72 }
73 }
74
75 return localLogin(req, res, next)
76}
77
78// ---------------------------------------------------------------------------
79
80export {
81 oAuthServer,
82 handleIdAndPassLogin,
83 onExternalAuthPlugin
84}
85
86// ---------------------------------------------------------------------------
87
88function localLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
89 return oAuthServer.token()(req, res, err => {
90 if (err) {
91 return res.status(err.status)
92 .json({
93 error: err.message,
94 code: err.name
95 })
96 .end()
97 }
98
99 return next()
100 })
101}
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts
index 086856f41..ea4a67802 100644
--- a/server/lib/oauth-model.ts
+++ b/server/lib/oauth-model.ts
@@ -1,4 +1,5 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as express from 'express'
2import { AccessDeniedError } from 'oauth2-server' 3import { AccessDeniedError } from 'oauth2-server'
3import { logger } from '../helpers/logger' 4import { logger } from '../helpers/logger'
4import { UserModel } from '../models/account/user' 5import { UserModel } from '../models/account/user'
@@ -9,6 +10,10 @@ import { Transaction } from 'sequelize'
9import { CONFIG } from '../initializers/config' 10import { CONFIG } from '../initializers/config'
10import * as LRUCache from 'lru-cache' 11import * as LRUCache from 'lru-cache'
11import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token' 12import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token'
13import { MUser } from '@server/typings/models/user/user'
14import { UserAdminFlag } from '@shared/models/users/user-flag.model'
15import { createUserAccountAndChannelAndPlaylist } from './user'
16import { UserRole } from '@shared/models/users/user-role'
12 17
13type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } 18type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
14 19
@@ -49,14 +54,14 @@ function getAccessToken (bearerToken: string) {
49 if (accessTokenCache.has(bearerToken)) return Bluebird.resolve(accessTokenCache.get(bearerToken)) 54 if (accessTokenCache.has(bearerToken)) return Bluebird.resolve(accessTokenCache.get(bearerToken))
50 55
51 return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) 56 return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)
52 .then(tokenModel => { 57 .then(tokenModel => {
53 if (tokenModel) { 58 if (tokenModel) {
54 accessTokenCache.set(bearerToken, tokenModel) 59 accessTokenCache.set(bearerToken, tokenModel)
55 userHavingToken.set(tokenModel.userId, tokenModel.accessToken) 60 userHavingToken.set(tokenModel.userId, tokenModel.accessToken)
56 } 61 }
57 62
58 return tokenModel 63 return tokenModel
59 }) 64 })
60} 65}
61 66
62function getClient (clientId: string, clientSecret: string) { 67function getClient (clientId: string, clientSecret: string) {
@@ -72,6 +77,20 @@ function getRefreshToken (refreshToken: string) {
72} 77}
73 78
74async function getUser (usernameOrEmail: string, password: string) { 79async function getUser (usernameOrEmail: string, password: string) {
80 const res: express.Response = this.request.res
81 if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) {
82 const obj = res.locals.bypassLogin
83 logger.info('Bypassing oauth login by plugin %s.', obj.pluginName)
84
85 let user = await UserModel.loadByEmail(obj.user.username)
86 if (!user) user = await createUserFromExternal(obj.pluginName, obj.user)
87
88 // This user does not belong to this plugin, skip it
89 if (user.pluginAuth !== obj.pluginName) return null
90
91 return user
92 }
93
75 logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).') 94 logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).')
76 95
77 const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail) 96 const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail)
@@ -96,19 +115,11 @@ async function revokeToken (tokenInfo: TokenInfo) {
96 115
97 token.destroy() 116 token.destroy()
98 .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) 117 .catch(err => logger.error('Cannot destroy token when revoking token.', { err }))
118
119 return true
99 } 120 }
100 121
101 /* 122 return false
102 * Thanks to https://github.com/manjeshpv/node-oauth2-server-implementation/blob/master/components/oauth/mongo-models.js
103 * "As per the discussion we need set older date
104 * revokeToken will expected return a boolean in future version
105 * https://github.com/oauthjs/node-oauth2-server/pull/274
106 * https://github.com/oauthjs/node-oauth2-server/issues/290"
107 */
108 const expiredToken = token
109 expiredToken.refreshTokenExpiresAt = new Date('2015-05-28T06:59:53.000Z')
110
111 return expiredToken
112} 123}
113 124
114async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { 125async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) {
@@ -141,3 +152,30 @@ export {
141 revokeToken, 152 revokeToken,
142 saveToken 153 saveToken
143} 154}
155
156async function createUserFromExternal (pluginAuth: string, options: {
157 username: string
158 email: string
159 role: UserRole
160 displayName: string
161}) {
162 const userToCreate = new UserModel({
163 username: options.username,
164 password: null,
165 email: options.email,
166 nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
167 autoPlayVideo: true,
168 role: options.role,
169 videoQuota: CONFIG.USER.VIDEO_QUOTA,
170 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY,
171 adminFlags: UserAdminFlag.NONE,
172 pluginAuth
173 }) as MUser
174
175 const { user } = await createUserAccountAndChannelAndPlaylist({
176 userToCreate,
177 userDisplayName: options.displayName
178 })
179
180 return user
181}
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index 37fb07716..f78b989f5 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -39,6 +39,7 @@ export interface RegisteredPlugin {
39 css: string[] 39 css: string[]
40 40
41 // Only if this is a plugin 41 // Only if this is a plugin
42 registerHelpersStore?: RegisterHelpersStore
42 unregister?: Function 43 unregister?: Function
43} 44}
44 45
@@ -58,11 +59,10 @@ export class PluginManager implements ServerHook {
58 private static instance: PluginManager 59 private static instance: PluginManager
59 60
60 private registeredPlugins: { [name: string]: RegisteredPlugin } = {} 61 private registeredPlugins: { [name: string]: RegisteredPlugin } = {}
62
61 private hooks: { [name: string]: HookInformationValue[] } = {} 63 private hooks: { [name: string]: HookInformationValue[] } = {}
62 private translations: PluginLocalesTranslations = {} 64 private translations: PluginLocalesTranslations = {}
63 65
64 private registerHelpersStore: { [npmName: string]: RegisterHelpersStore } = {}
65
66 private constructor () { 66 private constructor () {
67 } 67 }
68 68
@@ -102,18 +102,30 @@ export class PluginManager implements ServerHook {
102 return this.getRegisteredPluginsOrThemes(PluginType.THEME) 102 return this.getRegisteredPluginsOrThemes(PluginType.THEME)
103 } 103 }
104 104
105 getIdAndPassAuths () {
106 return this.getRegisteredPlugins()
107 .map(p => ({ npmName: p.npmName, idAndPassAuths: p.registerHelpersStore.getIdAndPassAuths() }))
108 .filter(v => v.idAndPassAuths.length !== 0)
109 }
110
111 getExternalAuths () {
112 return this.getRegisteredPlugins()
113 .map(p => ({ npmName: p.npmName, externalAuths: p.registerHelpersStore.getExternalAuths() }))
114 .filter(v => v.externalAuths.length !== 0)
115 }
116
105 getRegisteredSettings (npmName: string) { 117 getRegisteredSettings (npmName: string) {
106 const store = this.registerHelpersStore[npmName] 118 const result = this.getRegisteredPluginOrTheme(npmName)
107 if (store) return store.getSettings() 119 if (!result || result.type !== PluginType.PLUGIN) return []
108 120
109 return [] 121 return result.registerHelpersStore.getSettings()
110 } 122 }
111 123
112 getRouter (npmName: string) { 124 getRouter (npmName: string) {
113 const store = this.registerHelpersStore[npmName] 125 const result = this.getRegisteredPluginOrTheme(npmName)
114 if (!store) return null 126 if (!result || result.type !== PluginType.PLUGIN) return null
115 127
116 return store.getRouter() 128 return result.registerHelpersStore.getRouter()
117 } 129 }
118 130
119 getTranslations (locale: string) { 131 getTranslations (locale: string) {
@@ -185,11 +197,9 @@ export class PluginManager implements ServerHook {
185 this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName) 197 this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName)
186 } 198 }
187 199
188 const store = this.registerHelpersStore[plugin.npmName] 200 const store = plugin.registerHelpersStore
189 store.reinitVideoConstants(plugin.npmName) 201 store.reinitVideoConstants(plugin.npmName)
190 202
191 delete this.registerHelpersStore[plugin.npmName]
192
193 logger.info('Regenerating registered plugin CSS to global file.') 203 logger.info('Regenerating registered plugin CSS to global file.')
194 await this.regeneratePluginGlobalCSS() 204 await this.regeneratePluginGlobalCSS()
195 } 205 }
@@ -294,8 +304,11 @@ export class PluginManager implements ServerHook {
294 this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type) 304 this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type)
295 305
296 let library: PluginLibrary 306 let library: PluginLibrary
307 let registerHelpersStore: RegisterHelpersStore
297 if (plugin.type === PluginType.PLUGIN) { 308 if (plugin.type === PluginType.PLUGIN) {
298 library = await this.registerPlugin(plugin, pluginPath, packageJSON) 309 const result = await this.registerPlugin(plugin, pluginPath, packageJSON)
310 library = result.library
311 registerHelpersStore = result.registerStore
299 } 312 }
300 313
301 const clientScripts: { [id: string]: ClientScript } = {} 314 const clientScripts: { [id: string]: ClientScript } = {}
@@ -314,6 +327,7 @@ export class PluginManager implements ServerHook {
314 staticDirs: packageJSON.staticDirs, 327 staticDirs: packageJSON.staticDirs,
315 clientScripts, 328 clientScripts,
316 css: packageJSON.css, 329 css: packageJSON.css,
330 registerHelpersStore: registerHelpersStore || undefined,
317 unregister: library ? library.unregister : undefined 331 unregister: library ? library.unregister : undefined
318 } 332 }
319 333
@@ -332,15 +346,15 @@ export class PluginManager implements ServerHook {
332 throw new Error('Library code is not valid (miss register or unregister function)') 346 throw new Error('Library code is not valid (miss register or unregister function)')
333 } 347 }
334 348
335 const registerHelpers = this.getRegisterHelpers(npmName, plugin) 349 const { registerOptions, registerStore } = this.getRegisterHelpers(npmName, plugin)
336 library.register(registerHelpers) 350 library.register(registerOptions)
337 .catch(err => logger.error('Cannot register plugin %s.', npmName, { err })) 351 .catch(err => logger.error('Cannot register plugin %s.', npmName, { err }))
338 352
339 logger.info('Add plugin %s CSS to global file.', npmName) 353 logger.info('Add plugin %s CSS to global file.', npmName)
340 354
341 await this.addCSSToGlobalFile(pluginPath, packageJSON.css) 355 await this.addCSSToGlobalFile(pluginPath, packageJSON.css)
342 356
343 return library 357 return { library, registerStore }
344 } 358 }
345 359
346 // ###################### Translations ###################### 360 // ###################### Translations ######################
@@ -440,7 +454,10 @@ export class PluginManager implements ServerHook {
440 454
441 // ###################### Generate register helpers ###################### 455 // ###################### Generate register helpers ######################
442 456
443 private getRegisterHelpers (npmName: string, plugin: PluginModel): RegisterServerOptions { 457 private getRegisterHelpers (
458 npmName: string,
459 plugin: PluginModel
460 ): { registerStore: RegisterHelpersStore, registerOptions: RegisterServerOptions } {
444 const onHookAdded = (options: RegisterServerHookOptions) => { 461 const onHookAdded = (options: RegisterServerHookOptions) => {
445 if (!this.hooks[options.target]) this.hooks[options.target] = [] 462 if (!this.hooks[options.target]) this.hooks[options.target] = []
446 463
@@ -453,9 +470,11 @@ export class PluginManager implements ServerHook {
453 } 470 }
454 471
455 const registerHelpersStore = new RegisterHelpersStore(npmName, plugin, onHookAdded.bind(this)) 472 const registerHelpersStore = new RegisterHelpersStore(npmName, plugin, onHookAdded.bind(this))
456 this.registerHelpersStore[npmName] = registerHelpersStore
457 473
458 return registerHelpersStore.buildRegisterHelpers() 474 return {
475 registerStore: registerHelpersStore,
476 registerOptions: registerHelpersStore.buildRegisterHelpers()
477 }
459 } 478 }
460 479
461 private sanitizeAndCheckPackageJSONOrThrow (packageJSON: PluginPackageJson, pluginType: PluginType) { 480 private sanitizeAndCheckPackageJSONOrThrow (packageJSON: PluginPackageJson, pluginType: PluginType) {
diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers-store.ts
index 5ca52b151..7e827401f 100644
--- a/server/lib/plugins/register-helpers-store.ts
+++ b/server/lib/plugins/register-helpers-store.ts
@@ -20,6 +20,12 @@ import { RegisterServerSettingOptions } from '@shared/models/plugins/register-se
20import * as express from 'express' 20import * as express from 'express'
21import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model' 21import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model'
22import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model' 22import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model'
23import {
24 RegisterServerAuthExternalOptions,
25 RegisterServerAuthExternalResult,
26 RegisterServerAuthPassOptions
27} from '@shared/models/plugins/register-server-auth.model'
28import { onExternalAuthPlugin } from '@server/lib/auth'
23 29
24type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' 30type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
25type VideoConstant = { [key in number | string]: string } 31type VideoConstant = { [key in number | string]: string }
@@ -42,6 +48,9 @@ export class RegisterHelpersStore {
42 48
43 private readonly settings: RegisterServerSettingOptions[] = [] 49 private readonly settings: RegisterServerSettingOptions[] = []
44 50
51 private readonly idAndPassAuths: RegisterServerAuthPassOptions[] = []
52 private readonly externalAuths: RegisterServerAuthExternalOptions[] = []
53
45 private readonly router: express.Router 54 private readonly router: express.Router
46 55
47 constructor ( 56 constructor (
@@ -69,6 +78,9 @@ export class RegisterHelpersStore {
69 const videoPrivacyManager = this.buildVideoPrivacyManager() 78 const videoPrivacyManager = this.buildVideoPrivacyManager()
70 const playlistPrivacyManager = this.buildPlaylistPrivacyManager() 79 const playlistPrivacyManager = this.buildPlaylistPrivacyManager()
71 80
81 const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth()
82 const registerExternalAuth = this.buildRegisterExternalAuth()
83
72 const peertubeHelpers = buildPluginHelpers(this.npmName) 84 const peertubeHelpers = buildPluginHelpers(this.npmName)
73 85
74 return { 86 return {
@@ -87,6 +99,9 @@ export class RegisterHelpersStore {
87 videoPrivacyManager, 99 videoPrivacyManager,
88 playlistPrivacyManager, 100 playlistPrivacyManager,
89 101
102 registerIdAndPassAuth,
103 registerExternalAuth,
104
90 peertubeHelpers 105 peertubeHelpers
91 } 106 }
92 } 107 }
@@ -125,6 +140,14 @@ export class RegisterHelpersStore {
125 return this.router 140 return this.router
126 } 141 }
127 142
143 getIdAndPassAuths () {
144 return this.idAndPassAuths
145 }
146
147 getExternalAuths () {
148 return this.externalAuths
149 }
150
128 private buildGetRouter () { 151 private buildGetRouter () {
129 return () => this.router 152 return () => this.router
130 } 153 }
@@ -146,6 +169,26 @@ export class RegisterHelpersStore {
146 } 169 }
147 } 170 }
148 171
172 private buildRegisterIdAndPassAuth () {
173 return (options: RegisterServerAuthPassOptions) => {
174 this.idAndPassAuths.push(options)
175 }
176 }
177
178 private buildRegisterExternalAuth () {
179 const self = this
180
181 return (options: RegisterServerAuthExternalOptions) => {
182 this.externalAuths.push(options)
183
184 return {
185 onAuth (options: { username: string, email: string }): void {
186 onExternalAuthPlugin(self.npmName, options.username, options.email)
187 }
188 } as RegisterServerAuthExternalResult
189 }
190 }
191
149 private buildSettingsManager (): PluginSettingsManager { 192 private buildSettingsManager (): PluginSettingsManager {
150 return { 193 return {
151 getSetting: (name: string) => PluginModel.getSetting(this.plugin.name, this.plugin.type, name), 194 getSetting: (name: string) => PluginModel.getSetting(this.plugin.name, this.plugin.type, name),
diff --git a/server/lib/user.ts b/server/lib/user.ts
index 316c57359..8b447583e 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -2,7 +2,7 @@ import { v4 as uuidv4 } from 'uuid'
2import { ActivityPubActorType } from '../../shared/models/activitypub' 2import { ActivityPubActorType } from '../../shared/models/activitypub'
3import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' 3import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants'
4import { AccountModel } from '../models/account/account' 4import { AccountModel } from '../models/account/account'
5import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub' 5import { buildActorInstance, setAsyncActorKeys } from './activitypub/actor'
6import { createLocalVideoChannel } from './video-channel' 6import { createLocalVideoChannel } from './video-channel'
7import { ActorModel } from '../models/activitypub/actor' 7import { ActorModel } from '../models/activitypub/actor'
8import { UserNotificationSettingModel } from '../models/account/user-notification-setting' 8import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
@@ -14,6 +14,7 @@ import { Redis } from './redis'
14import { Emailer } from './emailer' 14import { Emailer } from './emailer'
15import { MAccountDefault, MActorDefault, MChannelActor } from '../typings/models' 15import { MAccountDefault, MActorDefault, MChannelActor } from '../typings/models'
16import { MUser, MUserDefault, MUserId } from '../typings/models/user' 16import { MUser, MUserDefault, MUserId } from '../typings/models/user'
17import { getAccountActivityPubUrl } from './activitypub/url'
17 18
18type ChannelNames = { name: string, displayName: string } 19type ChannelNames = { name: string, displayName: string }
19 20
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts
index c9887c667..102c1088d 100644
--- a/server/lib/video-channel.ts
+++ b/server/lib/video-channel.ts
@@ -2,9 +2,11 @@ import * as Sequelize from 'sequelize'
2import { v4 as uuidv4 } from 'uuid' 2import { v4 as uuidv4 } from 'uuid'
3import { VideoChannelCreate } from '../../shared/models' 3import { VideoChannelCreate } from '../../shared/models'
4import { VideoChannelModel } from '../models/video/video-channel' 4import { VideoChannelModel } from '../models/video/video-channel'
5import { buildActorInstance, federateVideoIfNeeded, getVideoChannelActivityPubUrl } from './activitypub' 5import { buildActorInstance } from './activitypub/actor'
6import { VideoModel } from '../models/video/video' 6import { VideoModel } from '../models/video/video'
7import { MAccountId, MChannelDefault, MChannelId } from '../typings/models' 7import { MAccountId, MChannelDefault, MChannelId } from '../typings/models'
8import { getVideoChannelActivityPubUrl } from './activitypub/url'
9import { federateVideoIfNeeded } from './activitypub/videos'
8 10
9type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T } 11type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T }
10 12
diff --git a/server/middlewares/oauth.ts b/server/middlewares/oauth.ts
index 9eef03bb4..4ae7f18c2 100644
--- a/server/middlewares/oauth.ts
+++ b/server/middlewares/oauth.ts
@@ -1,17 +1,8 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as OAuthServer from 'express-oauth-server'
3import { OAUTH_LIFETIME } from '../initializers/constants'
4import { logger } from '../helpers/logger' 2import { logger } from '../helpers/logger'
5import { Socket } from 'socket.io' 3import { Socket } from 'socket.io'
6import { getAccessToken } from '../lib/oauth-model' 4import { getAccessToken } from '../lib/oauth-model'
7 5import { handleIdAndPassLogin, oAuthServer } from '@server/lib/auth'
8const oAuthServer = new OAuthServer({
9 useErrorHandler: true,
10 accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
11 refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
12 continueMiddleware: true,
13 model: require('../lib/oauth-model')
14})
15 6
16function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) { 7function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) {
17 const options = authenticateInQuery ? { allowBearerTokensInQueryString: true } : {} 8 const options = authenticateInQuery ? { allowBearerTokensInQueryString: true } : {}
@@ -73,27 +64,11 @@ function optionalAuthenticate (req: express.Request, res: express.Response, next
73 return next() 64 return next()
74} 65}
75 66
76function token (req: express.Request, res: express.Response, next: express.NextFunction) {
77 return oAuthServer.token()(req, res, err => {
78 if (err) {
79 return res.status(err.status)
80 .json({
81 error: err.message,
82 code: err.name
83 })
84 .end()
85 }
86
87 return next()
88 })
89}
90
91// --------------------------------------------------------------------------- 67// ---------------------------------------------------------------------------
92 68
93export { 69export {
94 authenticate, 70 authenticate,
95 authenticateSocket, 71 authenticateSocket,
96 authenticatePromiseIfNeeded, 72 authenticatePromiseIfNeeded,
97 optionalAuthenticate, 73 optionalAuthenticate
98 token
99} 74}
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index da40bf290..d0d9a0508 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -221,7 +221,7 @@ enum ScopeNames {
221}) 221})
222export class UserModel extends Model<UserModel> { 222export class UserModel extends Model<UserModel> {
223 223
224 @AllowNull(false) 224 @AllowNull(true)
225 @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password')) 225 @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
226 @Column 226 @Column
227 password: string 227 password: string
@@ -348,6 +348,11 @@ export class UserModel extends Model<UserModel> {
348 @Column 348 @Column
349 noWelcomeModal: boolean 349 noWelcomeModal: boolean
350 350
351 @AllowNull(true)
352 @Default(null)
353 @Column
354 pluginAuth: string
355
351 @CreatedAt 356 @CreatedAt
352 createdAt: Date 357 createdAt: Date
353 358
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index db82e8fc2..7ba04a4ca 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -40,7 +40,7 @@ import {
40 getVideoAbusesList, updateCustomSubConfig, getCustomConfig, waitJobs 40 getVideoAbusesList, updateCustomSubConfig, getCustomConfig, waitJobs
41} from '../../../../shared/extra-utils' 41} from '../../../../shared/extra-utils'
42import { follow } from '../../../../shared/extra-utils/server/follows' 42import { follow } from '../../../../shared/extra-utils/server/follows'
43import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' 43import { setAccessTokensToServers, logout } from '../../../../shared/extra-utils/users/login'
44import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' 44import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
45import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 45import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
46import { CustomConfig } from '@shared/models/server' 46import { CustomConfig } from '@shared/models/server'
@@ -205,11 +205,17 @@ describe('Test users', function () {
205 }) 205 })
206 206
207 describe('Logout', function () { 207 describe('Logout', function () {
208 it('Should logout (revoke token)') 208 it('Should logout (revoke token)', async function () {
209 await logout(server.url, server.accessToken)
210 })
209 211
210 it('Should not be able to get the user information') 212 it('Should not be able to get the user information', async function () {
213 await getMyUserInformation(server.url, server.accessToken, 401)
214 })
211 215
212 it('Should not be able to upload a video') 216 it('Should not be able to upload a video', async function () {
217 await uploadVideo(server.url, server.accessToken, { name: 'video' }, 401)
218 })
213 219
214 it('Should not be able to remove a video') 220 it('Should not be able to remove a video')
215 221
diff --git a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/main.js b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/main.js
new file mode 100644
index 000000000..4755ed643
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/main.js
@@ -0,0 +1,61 @@
1async function register ({
2 registerIdAndPassAuth,
3 peertubeHelpers
4}) {
5 registerIdAndPassAuth({
6 type: 'id-and-pass',
7
8 onLogout: () => {
9 peertubeHelpers.logger.info('On logout for auth 1 - 1')
10 },
11
12 getWeight: () => 15,
13
14 login (body) {
15 if (body.id === 'spyro' && body.password === 'spyro password') {
16 return Promise.resolve({
17 username: 'spyro',
18 email: 'spyro@example.com',
19 role: 0,
20 displayName: 'Spyro the Dragon'
21 })
22 }
23
24 return null
25 }
26 })
27
28 registerIdAndPassAuth({
29 type: 'id-and-pass',
30
31 onLogout: () => {
32 peertubeHelpers.logger.info('On logout for auth 1 - 2')
33 },
34
35 getWeight: () => 50,
36
37 login (body) {
38 if (body.id === 'crash' && body.password === 'crash password') {
39 return Promise.resolve({
40 username: 'crash',
41 email: 'crash@example.com',
42 role: 2,
43 displayName: 'Crash Bandicoot'
44 })
45 }
46
47 return null
48 }
49 })
50}
51
52async function unregister () {
53 return
54}
55
56module.exports = {
57 register,
58 unregister
59}
60
61// ###########################################################################
diff --git a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/package.json b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/package.json
new file mode 100644
index 000000000..f8ad18a90
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/package.json
@@ -0,0 +1,20 @@
1{
2 "name": "peertube-plugin-test-id-pass-auth-one",
3 "version": "0.0.1",
4 "description": "Id and pass 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-id-pass-auth-three/main.js b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/main.js
new file mode 100644
index 000000000..2a15b3754
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/main.js
@@ -0,0 +1,37 @@
1async function register ({
2 registerIdAndPassAuth,
3 peertubeHelpers
4}) {
5 registerIdAndPassAuth({
6 type: 'id-and-pass',
7
8 onLogout: () => {
9 peertubeHelpers.logger.info('On logout for auth 3 - 1')
10 },
11
12 getWeight: () => 5,
13
14 login (body) {
15 if (body.id === 'laguna' && body.password === 'laguna password') {
16 return Promise.resolve({
17 username: 'laguna',
18 email: 'laguna@example.com',
19 displayName: 'Laguna Loire'
20 })
21 }
22
23 return null
24 }
25 })
26}
27
28async function unregister () {
29 return
30}
31
32module.exports = {
33 register,
34 unregister
35}
36
37// ###########################################################################
diff --git a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/package.json b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/package.json
new file mode 100644
index 000000000..f9f107b1a
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/package.json
@@ -0,0 +1,20 @@
1{
2 "name": "peertube-plugin-test-id-pass-auth-three",
3 "version": "0.0.1",
4 "description": "Id and pass auth three",
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-id-pass-auth-two/main.js b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js
new file mode 100644
index 000000000..edfc870c0
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js
@@ -0,0 +1,36 @@
1async function register ({
2 registerIdAndPassAuth,
3 peertubeHelpers
4}) {
5 registerIdAndPassAuth({
6 type: 'id-and-pass',
7
8 onLogout: () => {
9 peertubeHelpers.logger.info('On logout for auth 2 - 1')
10 },
11
12 getWeight: () => 30,
13
14 login (body) {
15 if (body.id === 'laguna' && body.password === 'laguna password') {
16 return Promise.resolve({
17 username: 'laguna',
18 email: 'laguna@example.com'
19 })
20 }
21
22 return null
23 }
24 })
25}
26
27async function unregister () {
28 return
29}
30
31module.exports = {
32 register,
33 unregister
34}
35
36// ###########################################################################
diff --git a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/package.json b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/package.json
new file mode 100644
index 000000000..5df15fac1
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/package.json
@@ -0,0 +1,20 @@
1{
2 "name": "peertube-plugin-test-id-pass-auth-two",
3 "version": "0.0.1",
4 "description": "Id and pass 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/id-and-pass-auth.ts b/server/tests/plugins/id-and-pass-auth.ts
new file mode 100644
index 000000000..5b4d1a1db
--- /dev/null
+++ b/server/tests/plugins/id-and-pass-auth.ts
@@ -0,0 +1,69 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
5import { getPluginTestPath, installPlugin, setAccessTokensToServers } from '../../../shared/extra-utils'
6
7describe('Test id and pass auth plugins', function () {
8 let server: ServerInfo
9
10 before(async function () {
11 this.timeout(30000)
12
13 server = await flushAndRunServer(1)
14 await setAccessTokensToServers([ server ])
15
16 await installPlugin({
17 url: server.url,
18 accessToken: server.accessToken,
19 path: getPluginTestPath('-id-pass-auth-one')
20 })
21
22 await installPlugin({
23 url: server.url,
24 accessToken: server.accessToken,
25 path: getPluginTestPath('-id-pass-auth-two')
26 })
27 })
28
29 it('Should not login', async function() {
30
31 })
32
33 it('Should login Spyro, create the user and use the token', async function() {
34
35 })
36
37 it('Should login Crash, create the user and use the token', async function() {
38
39 })
40
41 it('Should login the first Laguna, create the user and use the token', async function() {
42
43 })
44
45 it('Should update Crash profile', async function () {
46
47 })
48
49 it('Should logout Crash', async function () {
50
51 // test token
52 })
53
54 it('Should have logged the Crash logout', async function () {
55
56 })
57
58 it('Should login Crash and keep the old existing profile', async function () {
59
60 })
61
62 it('Should uninstall the plugin one and do not login existing Crash', async function () {
63
64 })
65
66 after(async function () {
67 await cleanupTests([ server ])
68 })
69})
diff --git a/server/tests/plugins/index.ts b/server/tests/plugins/index.ts
index 1414e7e58..8aa30654a 100644
--- a/server/tests/plugins/index.ts
+++ b/server/tests/plugins/index.ts
@@ -1,4 +1,5 @@
1import './action-hooks' 1import './action-hooks'
2import './id-and-pass-auth'
2import './filter-hooks' 3import './filter-hooks'
3import './translations' 4import './translations'
4import './video-constants' 5import './video-constants'
diff --git a/server/typings/express.ts b/server/typings/express.ts
index f4188bf3d..ebccf7f7d 100644
--- a/server/typings/express.ts
+++ b/server/typings/express.ts
@@ -28,12 +28,23 @@ import { MVideoChangeOwnershipFull } from './models/video/video-change-ownership
28import { MPlugin, MServer } from '@server/typings/models/server' 28import { MPlugin, MServer } from '@server/typings/models/server'
29import { MServerBlocklist } from './models/server/server-blocklist' 29import { MServerBlocklist } from './models/server/server-blocklist'
30import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token' 30import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token'
31import { UserRole } from '@shared/models'
31 32
32declare module 'express' { 33declare module 'express' {
33
34 interface Response { 34 interface Response {
35 35
36 locals: { 36 locals: {
37 bypassLogin?: {
38 bypass: boolean
39 pluginName: string
40 user: {
41 username: string
42 email: string
43 displayName: string
44 role: UserRole
45 }
46 }
47
37 videoAll?: MVideoFullLight 48 videoAll?: MVideoFullLight
38 onlyImmutableVideo?: MVideoImmutable 49 onlyImmutableVideo?: MVideoImmutable
39 onlyVideo?: MVideoThumbnail 50 onlyVideo?: MVideoThumbnail
diff --git a/server/typings/plugins/register-server-option.model.ts b/server/typings/plugins/register-server-option.model.ts
index 813e93003..0c0993c14 100644
--- a/server/typings/plugins/register-server-option.model.ts
+++ b/server/typings/plugins/register-server-option.model.ts
@@ -9,6 +9,7 @@ import { Logger } from 'winston'
9import { Router } from 'express' 9import { Router } from 'express'
10import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model' 10import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model'
11import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model' 11import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model'
12import { RegisterServerAuthPassOptions, RegisterServerAuthExternalOptions, RegisterServerAuthExternalResult } from '@shared/models/plugins/register-server-auth.model'
12 13
13export type PeerTubeHelpers = { 14export type PeerTubeHelpers = {
14 logger: Logger 15 logger: Logger
@@ -38,6 +39,9 @@ export type RegisterServerOptions = {
38 videoPrivacyManager: PluginVideoPrivacyManager 39 videoPrivacyManager: PluginVideoPrivacyManager
39 playlistPrivacyManager: PluginPlaylistPrivacyManager 40 playlistPrivacyManager: PluginPlaylistPrivacyManager
40 41
42 registerIdAndPassAuth: (options: RegisterServerAuthPassOptions) => void
43 registerExternalAuth: (options: RegisterServerAuthExternalOptions) => RegisterServerAuthExternalResult
44
41 // Get plugin router to create custom routes 45 // Get plugin router to create custom routes
42 // Base routes of this router are 46 // Base routes of this router are
43 // * /plugins/:pluginName/:pluginVersion/router/... 47 // * /plugins/:pluginName/:pluginVersion/router/...