aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/accounts.ts2
-rw-r--r--server/controllers/api/server/follows.ts2
-rw-r--r--server/controllers/api/server/server-blocklist.ts2
-rw-r--r--server/controllers/api/users/index.ts35
-rw-r--r--server/controllers/api/users/token.ts38
-rw-r--r--server/controllers/api/video-channel.ts2
-rw-r--r--server/controllers/api/video-playlist.ts2
-rw-r--r--server/lib/auth.ts50
-rw-r--r--server/lib/job-queue/job-queue.ts23
-rw-r--r--server/lib/oauth-model.ts19
-rw-r--r--server/lib/plugins/plugin-manager.ts20
-rw-r--r--server/lib/plugins/register-helpers-store.ts5
-rw-r--r--server/middlewares/oauth.ts2
-rw-r--r--server/middlewares/validators/themes.ts2
-rw-r--r--server/models/account/user.ts4
-rw-r--r--server/models/oauth/oauth-token.ts3
-rw-r--r--server/tests/api/users/users.ts34
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/main.js8
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/main.js2
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js2
-rw-r--r--server/tests/plugins/id-and-pass-auth.ts97
-rw-r--r--server/typings/express.ts3
-rw-r--r--server/typings/plugins/register-server-option.model.ts6
23 files changed, 266 insertions, 97 deletions
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 3bbb0a43e..ccdc610a2 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects} from '../../helpers/utils' 2import { getFormattedObjects } from '../../helpers/utils'
3import { 3import {
4 asyncMiddleware, 4 asyncMiddleware,
5 authenticate, 5 authenticate,
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts
index 82e9ef898..23823c9fb 100644
--- a/server/controllers/api/server/follows.ts
+++ b/server/controllers/api/server/follows.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2import { UserRight } from '../../../../shared/models/users' 2import { UserRight } from '../../../../shared/models/users'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getFormattedObjects} from '../../../helpers/utils' 4import { getFormattedObjects } from '../../../helpers/utils'
5import { SERVER_ACTOR_NAME } from '../../../initializers/constants' 5import { SERVER_ACTOR_NAME } from '../../../initializers/constants'
6import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send' 6import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send'
7import { 7import {
diff --git a/server/controllers/api/server/server-blocklist.ts b/server/controllers/api/server/server-blocklist.ts
index 008b8d4ea..f849b15c7 100644
--- a/server/controllers/api/server/server-blocklist.ts
+++ b/server/controllers/api/server/server-blocklist.ts
@@ -1,6 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import 'multer' 2import 'multer'
3import { getFormattedObjects} from '../../../helpers/utils' 3import { getFormattedObjects } from '../../../helpers/utils'
4import { 4import {
5 asyncMiddleware, 5 asyncMiddleware,
6 asyncRetryTransactionMiddleware, 6 asyncRetryTransactionMiddleware,
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index b30f42b43..c488f720b 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -26,12 +26,12 @@ import {
26 usersUpdateValidator 26 usersUpdateValidator
27} from '../../../middlewares' 27} from '../../../middlewares'
28import { 28import {
29 ensureCanManageUser,
29 usersAskResetPasswordValidator, 30 usersAskResetPasswordValidator,
30 usersAskSendVerifyEmailValidator, 31 usersAskSendVerifyEmailValidator,
31 usersBlockingValidator, 32 usersBlockingValidator,
32 usersResetPasswordValidator, 33 usersResetPasswordValidator,
33 usersVerifyEmailValidator, 34 usersVerifyEmailValidator
34 ensureCanManageUser
35} from '../../../middlewares/validators' 35} from '../../../middlewares/validators'
36import { UserModel } from '../../../models/account/user' 36import { UserModel } from '../../../models/account/user'
37import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' 37import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
@@ -49,15 +49,10 @@ import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
49import { UserRegister } from '../../../../shared/models/users/user-register.model' 49import { UserRegister } from '../../../../shared/models/users/user-register.model'
50import { MUser, MUserAccountDefault } from '@server/typings/models' 50import { MUser, MUserAccountDefault } from '@server/typings/models'
51import { Hooks } from '@server/lib/plugins/hooks' 51import { Hooks } from '@server/lib/plugins/hooks'
52import { handleIdAndPassLogin } from '@server/lib/auth' 52import { tokensRouter } from '@server/controllers/api/users/token'
53 53
54const auditLogger = auditLoggerFactory('users') 54const auditLogger = auditLoggerFactory('users')
55 55
56const loginRateLimiter = RateLimit({
57 windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
58 max: CONFIG.RATES_LIMIT.LOGIN.MAX
59})
60
61// @ts-ignore 56// @ts-ignore
62const signupRateLimiter = RateLimit({ 57const signupRateLimiter = RateLimit({
63 windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS, 58 windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS,
@@ -72,6 +67,7 @@ const askSendEmailLimiter = new RateLimit({
72}) 67})
73 68
74const usersRouter = express.Router() 69const usersRouter = express.Router()
70usersRouter.use('/', tokensRouter)
75usersRouter.use('/', myNotificationsRouter) 71usersRouter.use('/', myNotificationsRouter)
76usersRouter.use('/', mySubscriptionsRouter) 72usersRouter.use('/', mySubscriptionsRouter)
77usersRouter.use('/', myBlocklistRouter) 73usersRouter.use('/', myBlocklistRouter)
@@ -168,23 +164,6 @@ usersRouter.post('/:id/verify-email',
168 asyncMiddleware(verifyUserEmail) 164 asyncMiddleware(verifyUserEmail)
169) 165)
170 166
171usersRouter.post('/token',
172 loginRateLimiter,
173 handleIdAndPassLogin,
174 tokenSuccess
175)
176usersRouter.post('/token',
177 loginRateLimiter,
178 handleIdAndPassLogin,
179 tokenSuccess
180)
181usersRouter.post('/revoke-token',
182 loginRateLimiter,
183 handleIdAndPassLogin,
184 tokenSuccess
185)
186// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route
187
188// --------------------------------------------------------------------------- 167// ---------------------------------------------------------------------------
189 168
190export { 169export {
@@ -391,12 +370,6 @@ async function verifyUserEmail (req: express.Request, res: express.Response) {
391 return res.status(204).end() 370 return res.status(204).end()
392} 371}
393 372
394function tokenSuccess (req: express.Request) {
395 const username = req.body.username
396
397 Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip })
398}
399
400async function changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) { 373async function changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) {
401 const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) 374 const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
402 375
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts
new file mode 100644
index 000000000..9694f9e5e
--- /dev/null
+++ b/server/controllers/api/users/token.ts
@@ -0,0 +1,38 @@
1import { handleIdAndPassLogin, handleTokenRevocation } from '@server/lib/auth'
2import * as RateLimit from 'express-rate-limit'
3import { CONFIG } from '@server/initializers/config'
4import * as express from 'express'
5import { Hooks } from '@server/lib/plugins/hooks'
6import { asyncMiddleware, authenticate } from '@server/middlewares'
7
8const tokensRouter = express.Router()
9
10const loginRateLimiter = RateLimit({
11 windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
12 max: CONFIG.RATES_LIMIT.LOGIN.MAX
13})
14
15tokensRouter.post('/token',
16 loginRateLimiter,
17 handleIdAndPassLogin,
18 tokenSuccess
19)
20
21tokensRouter.post('/revoke-token',
22 authenticate,
23 asyncMiddleware(handleTokenRevocation),
24 tokenSuccess
25)
26
27// ---------------------------------------------------------------------------
28
29export {
30 tokensRouter
31}
32// ---------------------------------------------------------------------------
33
34function tokenSuccess (req: express.Request) {
35 const username = req.body.username
36
37 Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip })
38}
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index faef5ba4b..d779f1aab 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects} from '../../helpers/utils' 2import { getFormattedObjects } from '../../helpers/utils'
3import { 3import {
4 asyncMiddleware, 4 asyncMiddleware,
5 asyncRetryTransactionMiddleware, 5 asyncRetryTransactionMiddleware,
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts
index 49ac3c80e..375d711fd 100644
--- a/server/controllers/api/video-playlist.ts
+++ b/server/controllers/api/video-playlist.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects} from '../../helpers/utils' 2import { getFormattedObjects } from '../../helpers/utils'
3import { 3import {
4 asyncMiddleware, 4 asyncMiddleware,
5 asyncRetryTransactionMiddleware, 5 asyncRetryTransactionMiddleware,
diff --git a/server/lib/auth.ts b/server/lib/auth.ts
index 18d52fa5a..3495571db 100644
--- a/server/lib/auth.ts
+++ b/server/lib/auth.ts
@@ -5,6 +5,7 @@ import { PluginManager } from '@server/lib/plugins/plugin-manager'
5import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-server-auth.model' 5import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-server-auth.model'
6import { logger } from '@server/helpers/logger' 6import { logger } from '@server/helpers/logger'
7import { UserRole } from '@shared/models' 7import { UserRole } from '@shared/models'
8import { revokeToken } from '@server/lib/oauth-model'
8 9
9const oAuthServer = new OAuthServer({ 10const oAuthServer = new OAuthServer({
10 useErrorHandler: true, 11 useErrorHandler: true,
@@ -37,8 +38,9 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response
37 const aWeight = a.registerAuthOptions.getWeight() 38 const aWeight = a.registerAuthOptions.getWeight()
38 const bWeight = b.registerAuthOptions.getWeight() 39 const bWeight = b.registerAuthOptions.getWeight()
39 40
41 // DESC weight order
40 if (aWeight === bWeight) return 0 42 if (aWeight === bWeight) return 0
41 if (aWeight > bWeight) return 1 43 if (aWeight < bWeight) return 1
42 return -1 44 return -1
43 }) 45 })
44 46
@@ -48,18 +50,24 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response
48 } 50 }
49 51
50 for (const pluginAuth of pluginAuths) { 52 for (const pluginAuth of pluginAuths) {
53 const authOptions = pluginAuth.registerAuthOptions
54
51 logger.debug( 55 logger.debug(
52 'Using auth method of %s to login %s with weight %d.', 56 'Using auth method %s of plugin %s to login %s with weight %d.',
53 pluginAuth.npmName, loginOptions.id, pluginAuth.registerAuthOptions.getWeight() 57 authOptions.authName, pluginAuth.npmName, loginOptions.id, authOptions.getWeight()
54 ) 58 )
55 59
56 const loginResult = await pluginAuth.registerAuthOptions.login(loginOptions) 60 const loginResult = await authOptions.login(loginOptions)
57 if (loginResult) { 61 if (loginResult) {
58 logger.info('Login success with plugin %s for %s.', pluginAuth.npmName, loginOptions.id) 62 logger.info(
63 'Login success with auth method %s of plugin %s for %s.',
64 authOptions.authName, pluginAuth.npmName, loginOptions.id
65 )
59 66
60 res.locals.bypassLogin = { 67 res.locals.bypassLogin = {
61 bypass: true, 68 bypass: true,
62 pluginName: pluginAuth.npmName, 69 pluginName: pluginAuth.npmName,
70 authName: authOptions.authName,
63 user: { 71 user: {
64 username: loginResult.username, 72 username: loginResult.username,
65 email: loginResult.email, 73 email: loginResult.email,
@@ -75,12 +83,40 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response
75 return localLogin(req, res, next) 83 return localLogin(req, res, next)
76} 84}
77 85
86async function handleTokenRevocation (req: express.Request, res: express.Response) {
87 const token = res.locals.oauth.token
88
89 PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName)
90
91 await revokeToken(token)
92 .catch(err => {
93 logger.error('Cannot revoke token.', err)
94 })
95
96 // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released
97 // oAuthServer.revoke(req, res, err => {
98 // if (err) {
99 // logger.warn('Error in revoke token handler.', { err })
100 //
101 // return res.status(err.status)
102 // .json({
103 // error: err.message,
104 // code: err.name
105 // })
106 // .end()
107 // }
108 // })
109
110 return res.sendStatus(200)
111}
112
78// --------------------------------------------------------------------------- 113// ---------------------------------------------------------------------------
79 114
80export { 115export {
81 oAuthServer, 116 oAuthServer,
82 handleIdAndPassLogin, 117 handleIdAndPassLogin,
83 onExternalAuthPlugin 118 onExternalAuthPlugin,
119 handleTokenRevocation
84} 120}
85 121
86// --------------------------------------------------------------------------- 122// ---------------------------------------------------------------------------
@@ -88,6 +124,8 @@ export {
88function localLogin (req: express.Request, res: express.Response, next: express.NextFunction) { 124function localLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
89 return oAuthServer.token()(req, res, err => { 125 return oAuthServer.token()(req, res, err => {
90 if (err) { 126 if (err) {
127 logger.warn('Login error.', { err })
128
91 return res.status(err.status) 129 return res.status(err.status)
92 .json({ 130 .json({
93 error: err.message, 131 error: err.message,
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index d8d64caaf..14e181835 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -2,9 +2,16 @@ import * as Bull from 'bull'
2import { 2import {
3 ActivitypubFollowPayload, 3 ActivitypubFollowPayload,
4 ActivitypubHttpBroadcastPayload, 4 ActivitypubHttpBroadcastPayload,
5 ActivitypubHttpFetcherPayload, ActivitypubHttpUnicastPayload, EmailPayload, 5 ActivitypubHttpFetcherPayload,
6 ActivitypubHttpUnicastPayload,
7 EmailPayload,
6 JobState, 8 JobState,
7 JobType, RefreshPayload, VideoFileImportPayload, VideoImportPayload, VideoRedundancyPayload, VideoTranscodingPayload 9 JobType,
10 RefreshPayload,
11 VideoFileImportPayload,
12 VideoImportPayload,
13 VideoRedundancyPayload,
14 VideoTranscodingPayload
8} from '../../../shared/models' 15} from '../../../shared/models'
9import { logger } from '../../helpers/logger' 16import { logger } from '../../helpers/logger'
10import { Redis } from '../redis' 17import { Redis } from '../redis'
@@ -13,13 +20,13 @@ import { processActivityPubHttpBroadcast } from './handlers/activitypub-http-bro
13import { processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher' 20import { processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher'
14import { processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' 21import { processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast'
15import { processEmail } from './handlers/email' 22import { processEmail } from './handlers/email'
16import { processVideoTranscoding} from './handlers/video-transcoding' 23import { processVideoTranscoding } from './handlers/video-transcoding'
17import { processActivityPubFollow } from './handlers/activitypub-follow' 24import { processActivityPubFollow } from './handlers/activitypub-follow'
18import { processVideoImport} from './handlers/video-import' 25import { processVideoImport } from './handlers/video-import'
19import { processVideosViews } from './handlers/video-views' 26import { processVideosViews } from './handlers/video-views'
20import { refreshAPObject} from './handlers/activitypub-refresher' 27import { refreshAPObject } from './handlers/activitypub-refresher'
21import { processVideoFileImport} from './handlers/video-file-import' 28import { processVideoFileImport } from './handlers/video-file-import'
22import { processVideoRedundancy} from '@server/lib/job-queue/handlers/video-redundancy' 29import { processVideoRedundancy } from '@server/lib/job-queue/handlers/video-redundancy'
23 30
24type CreateJobArgument = 31type CreateJobArgument =
25 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | 32 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
@@ -117,7 +124,7 @@ class JobQueue {
117 124
118 createJob (obj: CreateJobArgument): void { 125 createJob (obj: CreateJobArgument): void {
119 this.createJobWithPromise(obj) 126 this.createJobWithPromise(obj)
120 .catch(err => logger.error('Cannot create job.', { err, obj })) 127 .catch(err => logger.error('Cannot create job.', { err, obj }))
121 } 128 }
122 129
123 createJobWithPromise (obj: CreateJobArgument) { 130 createJobWithPromise (obj: CreateJobArgument) {
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts
index ea4a67802..7a6ed63be 100644
--- a/server/lib/oauth-model.ts
+++ b/server/lib/oauth-model.ts
@@ -14,6 +14,7 @@ import { MUser } from '@server/typings/models/user/user'
14import { UserAdminFlag } from '@shared/models/users/user-flag.model' 14import { UserAdminFlag } from '@shared/models/users/user-flag.model'
15import { createUserAccountAndChannelAndPlaylist } from './user' 15import { createUserAccountAndChannelAndPlaylist } from './user'
16import { UserRole } from '@shared/models/users/user-role' 16import { UserRole } from '@shared/models/users/user-role'
17import { PluginManager } from '@server/lib/plugins/plugin-manager'
17 18
18type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } 19type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
19 20
@@ -82,7 +83,7 @@ async function getUser (usernameOrEmail: string, password: string) {
82 const obj = res.locals.bypassLogin 83 const obj = res.locals.bypassLogin
83 logger.info('Bypassing oauth login by plugin %s.', obj.pluginName) 84 logger.info('Bypassing oauth login by plugin %s.', obj.pluginName)
84 85
85 let user = await UserModel.loadByEmail(obj.user.username) 86 let user = await UserModel.loadByEmail(obj.user.email)
86 if (!user) user = await createUserFromExternal(obj.pluginName, obj.user) 87 if (!user) user = await createUserFromExternal(obj.pluginName, obj.user)
87 88
88 // This user does not belong to this plugin, skip it 89 // This user does not belong to this plugin, skip it
@@ -94,7 +95,8 @@ async function getUser (usernameOrEmail: string, password: string) {
94 logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).') 95 logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).')
95 96
96 const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail) 97 const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail)
97 if (!user) return null 98 // If we don't find the user, or if the user belongs to a plugin
99 if (!user || user.pluginAuth !== null) return null
98 100
99 const passwordMatch = await user.isPasswordMatch(password) 101 const passwordMatch = await user.isPasswordMatch(password)
100 if (passwordMatch === false) return null 102 if (passwordMatch === false) return null
@@ -109,8 +111,14 @@ async function getUser (usernameOrEmail: string, password: string) {
109} 111}
110 112
111async function revokeToken (tokenInfo: TokenInfo) { 113async function revokeToken (tokenInfo: TokenInfo) {
114 const res: express.Response = this.request.res
112 const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) 115 const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken)
116
113 if (token) { 117 if (token) {
118 if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) {
119 PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName)
120 }
121
114 clearCacheByToken(token.accessToken) 122 clearCacheByToken(token.accessToken)
115 123
116 token.destroy() 124 token.destroy()
@@ -123,6 +131,12 @@ async function revokeToken (tokenInfo: TokenInfo) {
123} 131}
124 132
125async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { 133async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) {
134 const res: express.Response = this.request.res
135
136 const authName = res.locals.bypassLogin?.bypass === true
137 ? res.locals.bypassLogin.authName
138 : null
139
126 logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') 140 logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.')
127 141
128 const tokenToCreate = { 142 const tokenToCreate = {
@@ -130,6 +144,7 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User
130 accessTokenExpiresAt: token.accessTokenExpiresAt, 144 accessTokenExpiresAt: token.accessTokenExpiresAt,
131 refreshToken: token.refreshToken, 145 refreshToken: token.refreshToken,
132 refreshTokenExpiresAt: token.refreshTokenExpiresAt, 146 refreshTokenExpiresAt: token.refreshTokenExpiresAt,
147 authName,
133 oAuthClientId: client.id, 148 oAuthClientId: client.id,
134 userId: user.id 149 userId: user.id
135 } 150 }
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index f78b989f5..9d646b689 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -76,7 +76,7 @@ export class PluginManager implements ServerHook {
76 return this.registeredPlugins[npmName] 76 return this.registeredPlugins[npmName]
77 } 77 }
78 78
79 getRegisteredPlugin (name: string) { 79 getRegisteredPluginByShortName (name: string) {
80 const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN) 80 const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN)
81 const registered = this.getRegisteredPluginOrTheme(npmName) 81 const registered = this.getRegisteredPluginOrTheme(npmName)
82 82
@@ -85,7 +85,7 @@ export class PluginManager implements ServerHook {
85 return registered 85 return registered
86 } 86 }
87 87
88 getRegisteredTheme (name: string) { 88 getRegisteredThemeByShortName (name: string) {
89 const npmName = PluginModel.buildNpmName(name, PluginType.THEME) 89 const npmName = PluginModel.buildNpmName(name, PluginType.THEME)
90 const registered = this.getRegisteredPluginOrTheme(npmName) 90 const registered = this.getRegisteredPluginOrTheme(npmName)
91 91
@@ -132,6 +132,22 @@ export class PluginManager implements ServerHook {
132 return this.translations[locale] || {} 132 return this.translations[locale] || {}
133 } 133 }
134 134
135 onLogout (npmName: string, authName: string) {
136 const plugin = this.getRegisteredPluginOrTheme(npmName)
137 if (!plugin || plugin.type !== PluginType.PLUGIN) return
138
139 const auth = plugin.registerHelpersStore.getIdAndPassAuths()
140 .find(a => a.authName === authName)
141
142 if (auth.onLogout) {
143 try {
144 auth.onLogout()
145 } catch (err) {
146 logger.warn('Cannot run onLogout function from auth %s of plugin %s.', authName, npmName, { err })
147 }
148 }
149 }
150
135 // ###################### Hooks ###################### 151 // ###################### Hooks ######################
136 152
137 async runHook<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> { 153 async runHook<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers-store.ts
index 7e827401f..679ed3650 100644
--- a/server/lib/plugins/register-helpers-store.ts
+++ b/server/lib/plugins/register-helpers-store.ts
@@ -171,6 +171,11 @@ export class RegisterHelpersStore {
171 171
172 private buildRegisterIdAndPassAuth () { 172 private buildRegisterIdAndPassAuth () {
173 return (options: RegisterServerAuthPassOptions) => { 173 return (options: RegisterServerAuthPassOptions) => {
174 if (!options.authName || typeof options.getWeight !== 'function' || typeof options.login !== 'function') {
175 logger.error('Cannot register auth plugin %s: authName of getWeight or login are not valid.', this.npmName)
176 return
177 }
178
174 this.idAndPassAuths.push(options) 179 this.idAndPassAuths.push(options)
175 } 180 }
176 } 181 }
diff --git a/server/middlewares/oauth.ts b/server/middlewares/oauth.ts
index 4ae7f18c2..9d0eaa51f 100644
--- a/server/middlewares/oauth.ts
+++ b/server/middlewares/oauth.ts
@@ -2,7 +2,7 @@ import * as express from 'express'
2import { logger } from '../helpers/logger' 2import { logger } from '../helpers/logger'
3import { Socket } from 'socket.io' 3import { Socket } from 'socket.io'
4import { getAccessToken } from '../lib/oauth-model' 4import { getAccessToken } from '../lib/oauth-model'
5import { handleIdAndPassLogin, oAuthServer } from '@server/lib/auth' 5import { oAuthServer } from '@server/lib/auth'
6 6
7function 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) {
8 const options = authenticateInQuery ? { allowBearerTokensInQueryString: true } : {} 8 const options = authenticateInQuery ? { allowBearerTokensInQueryString: true } : {}
diff --git a/server/middlewares/validators/themes.ts b/server/middlewares/validators/themes.ts
index 24a9673f7..82794656d 100644
--- a/server/middlewares/validators/themes.ts
+++ b/server/middlewares/validators/themes.ts
@@ -16,7 +16,7 @@ const serveThemeCSSValidator = [
16 16
17 if (areValidationErrors(req, res)) return 17 if (areValidationErrors(req, res)) return
18 18
19 const theme = PluginManager.Instance.getRegisteredTheme(req.params.themeName) 19 const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName)
20 20
21 if (!theme || theme.version !== req.params.themeVersion) { 21 if (!theme || theme.version !== req.params.themeVersion) {
22 return res.sendStatus(404) 22 return res.sendStatus(404)
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index d0d9a0508..1bff955df 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -222,7 +222,7 @@ enum ScopeNames {
222export class UserModel extends Model<UserModel> { 222export class UserModel extends Model<UserModel> {
223 223
224 @AllowNull(true) 224 @AllowNull(true)
225 @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password')) 225 @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true))
226 @Column 226 @Column
227 password: string 227 password: string
228 228
@@ -388,7 +388,7 @@ export class UserModel extends Model<UserModel> {
388 @BeforeCreate 388 @BeforeCreate
389 @BeforeUpdate 389 @BeforeUpdate
390 static cryptPasswordIfNeeded (instance: UserModel) { 390 static cryptPasswordIfNeeded (instance: UserModel) {
391 if (instance.changed('password')) { 391 if (instance.changed('password') && instance.password) {
392 return cryptPassword(instance.password) 392 return cryptPassword(instance.password)
393 .then(hash => { 393 .then(hash => {
394 instance.password = hash 394 instance.password = hash
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts
index d2101ce86..e73c4be7d 100644
--- a/server/models/oauth/oauth-token.ts
+++ b/server/models/oauth/oauth-token.ts
@@ -97,6 +97,9 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
97 @Column 97 @Column
98 refreshTokenExpiresAt: Date 98 refreshTokenExpiresAt: Date
99 99
100 @Column
101 authName: string
102
100 @CreatedAt 103 @CreatedAt
101 createdAt: Date 104 createdAt: Date
102 105
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 7ba04a4ca..60fbd2a20 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -2,8 +2,9 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { MyUser, User, UserRole, Video, VideoPlaylistType, VideoAbuseState, VideoAbuseUpdate } from '../../../../shared/index' 5import { MyUser, User, UserRole, Video, VideoAbuseState, VideoAbuseUpdate, VideoPlaylistType } from '../../../../shared/index'
6import { 6import {
7 addVideoCommentThread,
7 blockUser, 8 blockUser,
8 cleanupTests, 9 cleanupTests,
9 createUser, 10 createUser,
@@ -11,12 +12,14 @@ import {
11 flushAndRunServer, 12 flushAndRunServer,
12 getAccountRatings, 13 getAccountRatings,
13 getBlacklistedVideosList, 14 getBlacklistedVideosList,
15 getCustomConfig,
14 getMyUserInformation, 16 getMyUserInformation,
15 getMyUserVideoQuotaUsed, 17 getMyUserVideoQuotaUsed,
16 getMyUserVideoRating, 18 getMyUserVideoRating,
17 getUserInformation, 19 getUserInformation,
18 getUsersList, 20 getUsersList,
19 getUsersListPaginationAndSort, 21 getUsersListPaginationAndSort,
22 getVideoAbusesList,
20 getVideoChannel, 23 getVideoChannel,
21 getVideosList, 24 getVideosList,
22 installPlugin, 25 installPlugin,
@@ -26,21 +29,21 @@ import {
26 registerUserWithChannel, 29 registerUserWithChannel,
27 removeUser, 30 removeUser,
28 removeVideo, 31 removeVideo,
32 reportVideoAbuse,
29 ServerInfo, 33 ServerInfo,
30 testImage, 34 testImage,
31 unblockUser, 35 unblockUser,
36 updateCustomSubConfig,
32 updateMyAvatar, 37 updateMyAvatar,
33 updateMyUser, 38 updateMyUser,
34 updateUser, 39 updateUser,
40 updateVideoAbuse,
35 uploadVideo, 41 uploadVideo,
36 userLogin, 42 userLogin,
37 reportVideoAbuse, 43 waitJobs
38 addVideoCommentThread,
39 updateVideoAbuse,
40 getVideoAbusesList, updateCustomSubConfig, getCustomConfig, waitJobs
41} from '../../../../shared/extra-utils' 44} from '../../../../shared/extra-utils'
42import { follow } from '../../../../shared/extra-utils/server/follows' 45import { follow } from '../../../../shared/extra-utils/server/follows'
43import { setAccessTokensToServers, logout } from '../../../../shared/extra-utils/users/login' 46import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
44import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' 47import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
45import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 48import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
46import { CustomConfig } from '@shared/models/server' 49import { CustomConfig } from '@shared/models/server'
@@ -60,7 +63,14 @@ describe('Test users', function () {
60 63
61 before(async function () { 64 before(async function () {
62 this.timeout(30000) 65 this.timeout(30000)
63 server = await flushAndRunServer(1) 66
67 server = await flushAndRunServer(1, {
68 rates_limit: {
69 login: {
70 max: 30
71 }
72 }
73 })
64 74
65 await setAccessTokensToServers([ server ]) 75 await setAccessTokensToServers([ server ])
66 76
@@ -217,8 +227,6 @@ describe('Test users', function () {
217 await uploadVideo(server.url, server.accessToken, { name: 'video' }, 401) 227 await uploadVideo(server.url, server.accessToken, { name: 'video' }, 401)
218 }) 228 })
219 229
220 it('Should not be able to remove a video')
221
222 it('Should not be able to rate a video', async function () { 230 it('Should not be able to rate a video', async function () {
223 const path = '/api/v1/videos/' 231 const path = '/api/v1/videos/'
224 const data = { 232 const data = {
@@ -235,13 +243,17 @@ describe('Test users', function () {
235 await makePutBodyRequest(options) 243 await makePutBodyRequest(options)
236 }) 244 })
237 245
238 it('Should be able to login again') 246 it('Should be able to login again', async function () {
247 server.accessToken = await serverLogin(server)
248 })
239 249
240 it('Should have an expired access token') 250 it('Should have an expired access token')
241 251
242 it('Should refresh the token') 252 it('Should refresh the token')
243 253
244 it('Should be able to upload a video again') 254 it('Should be able to get my user information again', async function () {
255 await getMyUserInformation(server.url, server.accessToken)
256 })
245 }) 257 })
246 258
247 describe('Creating a user', function () { 259 describe('Creating a user', function () {
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
index 4755ed643..9fc12a3e3 100644
--- 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
@@ -3,7 +3,7 @@ async function register ({
3 peertubeHelpers 3 peertubeHelpers
4}) { 4}) {
5 registerIdAndPassAuth({ 5 registerIdAndPassAuth({
6 type: 'id-and-pass', 6 authName: 'spyro-auth',
7 7
8 onLogout: () => { 8 onLogout: () => {
9 peertubeHelpers.logger.info('On logout for auth 1 - 1') 9 peertubeHelpers.logger.info('On logout for auth 1 - 1')
@@ -16,7 +16,7 @@ async function register ({
16 return Promise.resolve({ 16 return Promise.resolve({
17 username: 'spyro', 17 username: 'spyro',
18 email: 'spyro@example.com', 18 email: 'spyro@example.com',
19 role: 0, 19 role: 2,
20 displayName: 'Spyro the Dragon' 20 displayName: 'Spyro the Dragon'
21 }) 21 })
22 } 22 }
@@ -26,7 +26,7 @@ async function register ({
26 }) 26 })
27 27
28 registerIdAndPassAuth({ 28 registerIdAndPassAuth({
29 type: 'id-and-pass', 29 authName: 'crash-auth',
30 30
31 onLogout: () => { 31 onLogout: () => {
32 peertubeHelpers.logger.info('On logout for auth 1 - 2') 32 peertubeHelpers.logger.info('On logout for auth 1 - 2')
@@ -39,7 +39,7 @@ async function register ({
39 return Promise.resolve({ 39 return Promise.resolve({
40 username: 'crash', 40 username: 'crash',
41 email: 'crash@example.com', 41 email: 'crash@example.com',
42 role: 2, 42 role: 1,
43 displayName: 'Crash Bandicoot' 43 displayName: 'Crash Bandicoot'
44 }) 44 })
45 } 45 }
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
index 2a15b3754..372f3fa0c 100644
--- 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
@@ -3,7 +3,7 @@ async function register ({
3 peertubeHelpers 3 peertubeHelpers
4}) { 4}) {
5 registerIdAndPassAuth({ 5 registerIdAndPassAuth({
6 type: 'id-and-pass', 6 authName: 'laguna-bad-auth',
7 7
8 onLogout: () => { 8 onLogout: () => {
9 peertubeHelpers.logger.info('On logout for auth 3 - 1') 9 peertubeHelpers.logger.info('On logout for auth 3 - 1')
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
index edfc870c0..c0e560019 100644
--- 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
@@ -3,7 +3,7 @@ async function register ({
3 peertubeHelpers 3 peertubeHelpers
4}) { 4}) {
5 registerIdAndPassAuth({ 5 registerIdAndPassAuth({
6 type: 'id-and-pass', 6 authName: 'laguna-auth',
7 7
8 onLogout: () => { 8 onLogout: () => {
9 peertubeHelpers.logger.info('On logout for auth 2 - 1') 9 peertubeHelpers.logger.info('On logout for auth 2 - 1')
diff --git a/server/tests/plugins/id-and-pass-auth.ts b/server/tests/plugins/id-and-pass-auth.ts
index 5b4d1a1db..45fa7856c 100644
--- a/server/tests/plugins/id-and-pass-auth.ts
+++ b/server/tests/plugins/id-and-pass-auth.ts
@@ -1,11 +1,23 @@
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 { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers' 4import { cleanupTests, flushAndRunServer, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
5import { getPluginTestPath, installPlugin, setAccessTokensToServers } from '../../../shared/extra-utils' 5import {
6 getMyUserInformation,
7 getPluginTestPath,
8 installPlugin,
9 logout,
10 setAccessTokensToServers,
11 uninstallPlugin,
12 updateMyUser,
13 userLogin
14} from '../../../shared/extra-utils'
15import { User, UserRole } from '@shared/models'
16import { expect } from 'chai'
6 17
7describe('Test id and pass auth plugins', function () { 18describe('Test id and pass auth plugins', function () {
8 let server: ServerInfo 19 let server: ServerInfo
20 let crashToken: string
9 21
10 before(async function () { 22 before(async function () {
11 this.timeout(30000) 23 this.timeout(30000)
@@ -13,54 +25,97 @@ describe('Test id and pass auth plugins', function () {
13 server = await flushAndRunServer(1) 25 server = await flushAndRunServer(1)
14 await setAccessTokensToServers([ server ]) 26 await setAccessTokensToServers([ server ])
15 27
16 await installPlugin({ 28 for (const suffix of [ 'one', 'two', 'three' ]) {
17 url: server.url, 29 await installPlugin({
18 accessToken: server.accessToken, 30 url: server.url,
19 path: getPluginTestPath('-id-pass-auth-one') 31 accessToken: server.accessToken,
20 }) 32 path: getPluginTestPath('-id-pass-auth-' + suffix)
21 33 })
22 await installPlugin({ 34 }
23 url: server.url,
24 accessToken: server.accessToken,
25 path: getPluginTestPath('-id-pass-auth-two')
26 })
27 }) 35 })
28 36
29 it('Should not login', async function() { 37 it('Should not login', async function () {
30 38 await userLogin(server, { username: 'toto', password: 'password' }, 400)
31 }) 39 })
32 40
33 it('Should login Spyro, create the user and use the token', async function() { 41 it('Should login Spyro, create the user and use the token', async function () {
42 const accessToken = await userLogin(server, { username: 'spyro', password: 'spyro password' })
34 43
44 const res = await getMyUserInformation(server.url, accessToken)
45
46 const body: User = res.body
47 expect(body.username).to.equal('spyro')
48 expect(body.account.displayName).to.equal('Spyro the Dragon')
49 expect(body.role).to.equal(UserRole.USER)
35 }) 50 })
36 51
37 it('Should login Crash, create the user and use the token', async function() { 52 it('Should login Crash, create the user and use the token', async function () {
53 crashToken = await userLogin(server, { username: 'crash', password: 'crash password' })
54
55 const res = await getMyUserInformation(server.url, crashToken)
38 56
57 const body: User = res.body
58 expect(body.username).to.equal('crash')
59 expect(body.account.displayName).to.equal('Crash Bandicoot')
60 expect(body.role).to.equal(UserRole.MODERATOR)
39 }) 61 })
40 62
41 it('Should login the first Laguna, create the user and use the token', async function() { 63 it('Should login the first Laguna, create the user and use the token', async function () {
64 const accessToken = await userLogin(server, { username: 'laguna', password: 'laguna password' })
42 65
66 const res = await getMyUserInformation(server.url, accessToken)
67
68 const body: User = res.body
69 expect(body.username).to.equal('laguna')
70 expect(body.account.displayName).to.equal('laguna')
71 expect(body.role).to.equal(UserRole.USER)
43 }) 72 })
44 73
45 it('Should update Crash profile', async function () { 74 it('Should update Crash profile', async function () {
75 await updateMyUser({
76 url: server.url,
77 accessToken: crashToken,
78 displayName: 'Beautiful Crash',
79 description: 'Mutant eastern barred bandicoot'
80 })
46 81
82 const res = await getMyUserInformation(server.url, crashToken)
83
84 const body: User = res.body
85 expect(body.account.displayName).to.equal('Beautiful Crash')
86 expect(body.account.description).to.equal('Mutant eastern barred bandicoot')
47 }) 87 })
48 88
49 it('Should logout Crash', async function () { 89 it('Should logout Crash', async function () {
50 90 await logout(server.url, crashToken)
51 // test token
52 }) 91 })
53 92
54 it('Should have logged the Crash logout', async function () { 93 it('Should have logged out Crash', async function () {
94 await getMyUserInformation(server.url, crashToken, 401)
55 95
96 await waitUntilLog(server, 'On logout for auth 1 - 2')
56 }) 97 })
57 98
58 it('Should login Crash and keep the old existing profile', async function () { 99 it('Should login Crash and keep the old existing profile', async function () {
100 crashToken = await userLogin(server, { username: 'crash', password: 'crash password' })
59 101
102 const res = await getMyUserInformation(server.url, crashToken)
103
104 const body: User = res.body
105 expect(body.username).to.equal('crash')
106 expect(body.account.displayName).to.equal('Beautiful Crash')
107 expect(body.account.description).to.equal('Mutant eastern barred bandicoot')
108 expect(body.role).to.equal(UserRole.MODERATOR)
60 }) 109 })
61 110
62 it('Should uninstall the plugin one and do not login existing Crash', async function () { 111 it('Should uninstall the plugin one and do not login existing Crash', async function () {
112 await uninstallPlugin({
113 url: server.url,
114 accessToken: server.accessToken,
115 npmName: 'peertube-plugin-test-id-pass-auth-one'
116 })
63 117
118 await userLogin(server, { username: 'crash', password: 'crash password' }, 400)
64 }) 119 })
65 120
66 after(async function () { 121 after(async function () {
diff --git a/server/typings/express.ts b/server/typings/express.ts
index ebccf7f7d..2d12a486a 100644
--- a/server/typings/express.ts
+++ b/server/typings/express.ts
@@ -37,6 +37,7 @@ declare module 'express' {
37 bypassLogin?: { 37 bypassLogin?: {
38 bypass: boolean 38 bypass: boolean
39 pluginName: string 39 pluginName: string
40 authName?: string
40 user: { 41 user: {
41 username: string 42 username: string
42 email: string 43 email: string
@@ -45,6 +46,8 @@ declare module 'express' {
45 } 46 }
46 } 47 }
47 48
49 explicitLogout: boolean
50
48 videoAll?: MVideoFullLight 51 videoAll?: MVideoFullLight
49 onlyImmutableVideo?: MVideoImmutable 52 onlyImmutableVideo?: MVideoImmutable
50 onlyVideo?: MVideoThumbnail 53 onlyVideo?: MVideoThumbnail
diff --git a/server/typings/plugins/register-server-option.model.ts b/server/typings/plugins/register-server-option.model.ts
index 0c0993c14..bcabf2fec 100644
--- a/server/typings/plugins/register-server-option.model.ts
+++ b/server/typings/plugins/register-server-option.model.ts
@@ -9,7 +9,11 @@ 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' 12import {
13 RegisterServerAuthExternalOptions,
14 RegisterServerAuthExternalResult,
15 RegisterServerAuthPassOptions
16} from '@shared/models/plugins/register-server-auth.model'
13 17
14export type PeerTubeHelpers = { 18export type PeerTubeHelpers = {
15 logger: Logger 19 logger: Logger