From e1c5503114deef954731904695cd40dccfcef555 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 23 Apr 2020 11:36:50 +0200 Subject: [PATCH] Support logout and add id and pass tests --- scripts/update-host.ts | 4 +- server/controllers/api/accounts.ts | 2 +- server/controllers/api/server/follows.ts | 2 +- .../api/server/server-blocklist.ts | 2 +- server/controllers/api/users/index.ts | 35 +------ server/controllers/api/users/token.ts | 38 ++++++++ server/controllers/api/video-channel.ts | 2 +- server/controllers/api/video-playlist.ts | 2 +- server/lib/auth.ts | 50 ++++++++-- server/lib/job-queue/job-queue.ts | 23 +++-- server/lib/oauth-model.ts | 19 +++- server/lib/plugins/plugin-manager.ts | 20 +++- server/lib/plugins/register-helpers-store.ts | 5 + server/middlewares/oauth.ts | 2 +- server/middlewares/validators/themes.ts | 2 +- server/models/account/user.ts | 4 +- server/models/oauth/oauth-token.ts | 3 + server/tests/api/users/users.ts | 34 ++++--- .../main.js | 8 +- .../main.js | 2 +- .../main.js | 2 +- server/tests/plugins/id-and-pass-auth.ts | 97 +++++++++++++++---- server/typings/express.ts | 3 + .../plugins/register-server-option.model.ts | 6 +- .../plugins/register-server-auth.model.ts | 7 +- 25 files changed, 273 insertions(+), 101 deletions(-) create mode 100644 server/controllers/api/users/token.ts diff --git a/scripts/update-host.ts b/scripts/update-host.ts index 54b31d786..7b07dea04 100755 --- a/scripts/update-host.ts +++ b/scripts/update-host.ts @@ -11,15 +11,15 @@ import { getVideoAnnounceActivityPubUrl, getVideoChannelActivityPubUrl, getVideoCommentActivityPubUrl -} from '../server/lib/activitypub' +} from '../server/lib/activitypub/url' import { VideoShareModel } from '../server/models/video/video-share' import { VideoCommentModel } from '../server/models/video/video-comment' -import { getServerActor } from '../server/helpers/utils' import { AccountModel } from '../server/models/account/account' import { VideoChannelModel } from '../server/models/video/video-channel' import { VideoStreamingPlaylistModel } from '../server/models/video/video-streaming-playlist' import { initDatabaseModels } from '../server/initializers' import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' +import { getServerActor } from '@server/models/application/application' run() .then(() => process.exit(0)) 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 @@ import * as express from 'express' -import { getFormattedObjects} from '../../helpers/utils' +import { getFormattedObjects } from '../../helpers/utils' import { asyncMiddleware, 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 @@ import * as express from 'express' import { UserRight } from '../../../../shared/models/users' import { logger } from '../../../helpers/logger' -import { getFormattedObjects} from '../../../helpers/utils' +import { getFormattedObjects } from '../../../helpers/utils' import { SERVER_ACTOR_NAME } from '../../../initializers/constants' import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send' import { 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 @@ import * as express from 'express' import 'multer' -import { getFormattedObjects} from '../../../helpers/utils' +import { getFormattedObjects } from '../../../helpers/utils' import { asyncMiddleware, 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 { usersUpdateValidator } from '../../../middlewares' import { + ensureCanManageUser, usersAskResetPasswordValidator, usersAskSendVerifyEmailValidator, usersBlockingValidator, usersResetPasswordValidator, - usersVerifyEmailValidator, - ensureCanManageUser + usersVerifyEmailValidator } from '../../../middlewares/validators' import { UserModel } from '../../../models/account/user' import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' @@ -49,15 +49,10 @@ import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' import { UserRegister } from '../../../../shared/models/users/user-register.model' import { MUser, MUserAccountDefault } from '@server/typings/models' import { Hooks } from '@server/lib/plugins/hooks' -import { handleIdAndPassLogin } from '@server/lib/auth' +import { tokensRouter } from '@server/controllers/api/users/token' const auditLogger = auditLoggerFactory('users') -const loginRateLimiter = RateLimit({ - windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS, - max: CONFIG.RATES_LIMIT.LOGIN.MAX -}) - // @ts-ignore const signupRateLimiter = RateLimit({ windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS, @@ -72,6 +67,7 @@ const askSendEmailLimiter = new RateLimit({ }) const usersRouter = express.Router() +usersRouter.use('/', tokensRouter) usersRouter.use('/', myNotificationsRouter) usersRouter.use('/', mySubscriptionsRouter) usersRouter.use('/', myBlocklistRouter) @@ -168,23 +164,6 @@ usersRouter.post('/:id/verify-email', asyncMiddleware(verifyUserEmail) ) -usersRouter.post('/token', - loginRateLimiter, - handleIdAndPassLogin, - tokenSuccess -) -usersRouter.post('/token', - loginRateLimiter, - handleIdAndPassLogin, - tokenSuccess -) -usersRouter.post('/revoke-token', - loginRateLimiter, - handleIdAndPassLogin, - tokenSuccess -) -// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route - // --------------------------------------------------------------------------- export { @@ -391,12 +370,6 @@ async function verifyUserEmail (req: express.Request, res: express.Response) { return res.status(204).end() } -function tokenSuccess (req: express.Request) { - const username = req.body.username - - Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip }) -} - async function changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) { const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) 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 @@ +import { handleIdAndPassLogin, handleTokenRevocation } from '@server/lib/auth' +import * as RateLimit from 'express-rate-limit' +import { CONFIG } from '@server/initializers/config' +import * as express from 'express' +import { Hooks } from '@server/lib/plugins/hooks' +import { asyncMiddleware, authenticate } from '@server/middlewares' + +const tokensRouter = express.Router() + +const loginRateLimiter = RateLimit({ + windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS, + max: CONFIG.RATES_LIMIT.LOGIN.MAX +}) + +tokensRouter.post('/token', + loginRateLimiter, + handleIdAndPassLogin, + tokenSuccess +) + +tokensRouter.post('/revoke-token', + authenticate, + asyncMiddleware(handleTokenRevocation), + tokenSuccess +) + +// --------------------------------------------------------------------------- + +export { + tokensRouter +} +// --------------------------------------------------------------------------- + +function tokenSuccess (req: express.Request) { + const username = req.body.username + + Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip }) +} 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 @@ import * as express from 'express' -import { getFormattedObjects} from '../../helpers/utils' +import { getFormattedObjects } from '../../helpers/utils' import { asyncMiddleware, 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 @@ import * as express from 'express' -import { getFormattedObjects} from '../../helpers/utils' +import { getFormattedObjects } from '../../helpers/utils' import { asyncMiddleware, 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' import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-server-auth.model' import { logger } from '@server/helpers/logger' import { UserRole } from '@shared/models' +import { revokeToken } from '@server/lib/oauth-model' const oAuthServer = new OAuthServer({ useErrorHandler: true, @@ -37,8 +38,9 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response const aWeight = a.registerAuthOptions.getWeight() const bWeight = b.registerAuthOptions.getWeight() + // DESC weight order if (aWeight === bWeight) return 0 - if (aWeight > bWeight) return 1 + if (aWeight < bWeight) return 1 return -1 }) @@ -48,18 +50,24 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response } for (const pluginAuth of pluginAuths) { + const authOptions = pluginAuth.registerAuthOptions + logger.debug( - 'Using auth method of %s to login %s with weight %d.', - pluginAuth.npmName, loginOptions.id, pluginAuth.registerAuthOptions.getWeight() + 'Using auth method %s of plugin %s to login %s with weight %d.', + authOptions.authName, pluginAuth.npmName, loginOptions.id, authOptions.getWeight() ) - const loginResult = await pluginAuth.registerAuthOptions.login(loginOptions) + const loginResult = await authOptions.login(loginOptions) if (loginResult) { - logger.info('Login success with plugin %s for %s.', pluginAuth.npmName, loginOptions.id) + logger.info( + 'Login success with auth method %s of plugin %s for %s.', + authOptions.authName, pluginAuth.npmName, loginOptions.id + ) res.locals.bypassLogin = { bypass: true, pluginName: pluginAuth.npmName, + authName: authOptions.authName, user: { username: loginResult.username, email: loginResult.email, @@ -75,12 +83,40 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response return localLogin(req, res, next) } +async function handleTokenRevocation (req: express.Request, res: express.Response) { + const token = res.locals.oauth.token + + PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName) + + await revokeToken(token) + .catch(err => { + logger.error('Cannot revoke token.', err) + }) + + // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released + // oAuthServer.revoke(req, res, err => { + // if (err) { + // logger.warn('Error in revoke token handler.', { err }) + // + // return res.status(err.status) + // .json({ + // error: err.message, + // code: err.name + // }) + // .end() + // } + // }) + + return res.sendStatus(200) +} + // --------------------------------------------------------------------------- export { oAuthServer, handleIdAndPassLogin, - onExternalAuthPlugin + onExternalAuthPlugin, + handleTokenRevocation } // --------------------------------------------------------------------------- @@ -88,6 +124,8 @@ export { function localLogin (req: express.Request, res: express.Response, next: express.NextFunction) { return oAuthServer.token()(req, res, err => { if (err) { + logger.warn('Login error.', { err }) + return res.status(err.status) .json({ 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' import { ActivitypubFollowPayload, ActivitypubHttpBroadcastPayload, - ActivitypubHttpFetcherPayload, ActivitypubHttpUnicastPayload, EmailPayload, + ActivitypubHttpFetcherPayload, + ActivitypubHttpUnicastPayload, + EmailPayload, JobState, - JobType, RefreshPayload, VideoFileImportPayload, VideoImportPayload, VideoRedundancyPayload, VideoTranscodingPayload + JobType, + RefreshPayload, + VideoFileImportPayload, + VideoImportPayload, + VideoRedundancyPayload, + VideoTranscodingPayload } from '../../../shared/models' import { logger } from '../../helpers/logger' import { Redis } from '../redis' @@ -13,13 +20,13 @@ import { processActivityPubHttpBroadcast } from './handlers/activitypub-http-bro import { processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher' import { processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' import { processEmail } from './handlers/email' -import { processVideoTranscoding} from './handlers/video-transcoding' +import { processVideoTranscoding } from './handlers/video-transcoding' import { processActivityPubFollow } from './handlers/activitypub-follow' -import { processVideoImport} from './handlers/video-import' +import { processVideoImport } from './handlers/video-import' import { processVideosViews } from './handlers/video-views' -import { refreshAPObject} from './handlers/activitypub-refresher' -import { processVideoFileImport} from './handlers/video-file-import' -import { processVideoRedundancy} from '@server/lib/job-queue/handlers/video-redundancy' +import { refreshAPObject } from './handlers/activitypub-refresher' +import { processVideoFileImport } from './handlers/video-file-import' +import { processVideoRedundancy } from '@server/lib/job-queue/handlers/video-redundancy' type CreateJobArgument = { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | @@ -117,7 +124,7 @@ class JobQueue { createJob (obj: CreateJobArgument): void { this.createJobWithPromise(obj) - .catch(err => logger.error('Cannot create job.', { err, obj })) + .catch(err => logger.error('Cannot create job.', { err, obj })) } 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' import { UserAdminFlag } from '@shared/models/users/user-flag.model' import { createUserAccountAndChannelAndPlaylist } from './user' import { UserRole } from '@shared/models/users/user-role' +import { PluginManager } from '@server/lib/plugins/plugin-manager' type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } @@ -82,7 +83,7 @@ async function getUser (usernameOrEmail: string, password: string) { const obj = res.locals.bypassLogin logger.info('Bypassing oauth login by plugin %s.', obj.pluginName) - let user = await UserModel.loadByEmail(obj.user.username) + let user = await UserModel.loadByEmail(obj.user.email) if (!user) user = await createUserFromExternal(obj.pluginName, obj.user) // This user does not belong to this plugin, skip it @@ -94,7 +95,8 @@ async function getUser (usernameOrEmail: string, password: string) { logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).') const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail) - if (!user) return null + // If we don't find the user, or if the user belongs to a plugin + if (!user || user.pluginAuth !== null) return null const passwordMatch = await user.isPasswordMatch(password) if (passwordMatch === false) return null @@ -109,8 +111,14 @@ async function getUser (usernameOrEmail: string, password: string) { } async function revokeToken (tokenInfo: TokenInfo) { + const res: express.Response = this.request.res const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) + if (token) { + if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) { + PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName) + } + clearCacheByToken(token.accessToken) token.destroy() @@ -123,6 +131,12 @@ async function revokeToken (tokenInfo: TokenInfo) { } async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { + const res: express.Response = this.request.res + + const authName = res.locals.bypassLogin?.bypass === true + ? res.locals.bypassLogin.authName + : null + logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') const tokenToCreate = { @@ -130,6 +144,7 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User accessTokenExpiresAt: token.accessTokenExpiresAt, refreshToken: token.refreshToken, refreshTokenExpiresAt: token.refreshTokenExpiresAt, + authName, oAuthClientId: client.id, userId: user.id } 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 { return this.registeredPlugins[npmName] } - getRegisteredPlugin (name: string) { + getRegisteredPluginByShortName (name: string) { const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN) const registered = this.getRegisteredPluginOrTheme(npmName) @@ -85,7 +85,7 @@ export class PluginManager implements ServerHook { return registered } - getRegisteredTheme (name: string) { + getRegisteredThemeByShortName (name: string) { const npmName = PluginModel.buildNpmName(name, PluginType.THEME) const registered = this.getRegisteredPluginOrTheme(npmName) @@ -132,6 +132,22 @@ export class PluginManager implements ServerHook { return this.translations[locale] || {} } + onLogout (npmName: string, authName: string) { + const plugin = this.getRegisteredPluginOrTheme(npmName) + if (!plugin || plugin.type !== PluginType.PLUGIN) return + + const auth = plugin.registerHelpersStore.getIdAndPassAuths() + .find(a => a.authName === authName) + + if (auth.onLogout) { + try { + auth.onLogout() + } catch (err) { + logger.warn('Cannot run onLogout function from auth %s of plugin %s.', authName, npmName, { err }) + } + } + } + // ###################### Hooks ###################### async runHook (hookName: ServerHookName, result?: T, params?: any): Promise { 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 { private buildRegisterIdAndPassAuth () { return (options: RegisterServerAuthPassOptions) => { + if (!options.authName || typeof options.getWeight !== 'function' || typeof options.login !== 'function') { + logger.error('Cannot register auth plugin %s: authName of getWeight or login are not valid.', this.npmName) + return + } + this.idAndPassAuths.push(options) } } 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' import { logger } from '../helpers/logger' import { Socket } from 'socket.io' import { getAccessToken } from '../lib/oauth-model' -import { handleIdAndPassLogin, oAuthServer } from '@server/lib/auth' +import { oAuthServer } from '@server/lib/auth' function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) { 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 = [ if (areValidationErrors(req, res)) return - const theme = PluginManager.Instance.getRegisteredTheme(req.params.themeName) + const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName) if (!theme || theme.version !== req.params.themeVersion) { 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 { export class UserModel extends Model { @AllowNull(true) - @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password')) + @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true)) @Column password: string @@ -388,7 +388,7 @@ export class UserModel extends Model { @BeforeCreate @BeforeUpdate static cryptPasswordIfNeeded (instance: UserModel) { - if (instance.changed('password')) { + if (instance.changed('password') && instance.password) { return cryptPassword(instance.password) .then(hash => { 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 { @Column refreshTokenExpiresAt: Date + @Column + authName: string + @CreatedAt createdAt: Date 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 @@ import * as chai from 'chai' import 'mocha' -import { MyUser, User, UserRole, Video, VideoPlaylistType, VideoAbuseState, VideoAbuseUpdate } from '../../../../shared/index' +import { MyUser, User, UserRole, Video, VideoAbuseState, VideoAbuseUpdate, VideoPlaylistType } from '../../../../shared/index' import { + addVideoCommentThread, blockUser, cleanupTests, createUser, @@ -11,12 +12,14 @@ import { flushAndRunServer, getAccountRatings, getBlacklistedVideosList, + getCustomConfig, getMyUserInformation, getMyUserVideoQuotaUsed, getMyUserVideoRating, getUserInformation, getUsersList, getUsersListPaginationAndSort, + getVideoAbusesList, getVideoChannel, getVideosList, installPlugin, @@ -26,21 +29,21 @@ import { registerUserWithChannel, removeUser, removeVideo, + reportVideoAbuse, ServerInfo, testImage, unblockUser, + updateCustomSubConfig, updateMyAvatar, updateMyUser, updateUser, + updateVideoAbuse, uploadVideo, userLogin, - reportVideoAbuse, - addVideoCommentThread, - updateVideoAbuse, - getVideoAbusesList, updateCustomSubConfig, getCustomConfig, waitJobs + waitJobs } from '../../../../shared/extra-utils' import { follow } from '../../../../shared/extra-utils/server/follows' -import { setAccessTokensToServers, logout } from '../../../../shared/extra-utils/users/login' +import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' import { CustomConfig } from '@shared/models/server' @@ -60,7 +63,14 @@ describe('Test users', function () { before(async function () { this.timeout(30000) - server = await flushAndRunServer(1) + + server = await flushAndRunServer(1, { + rates_limit: { + login: { + max: 30 + } + } + }) await setAccessTokensToServers([ server ]) @@ -217,8 +227,6 @@ describe('Test users', function () { await uploadVideo(server.url, server.accessToken, { name: 'video' }, 401) }) - it('Should not be able to remove a video') - it('Should not be able to rate a video', async function () { const path = '/api/v1/videos/' const data = { @@ -235,13 +243,17 @@ describe('Test users', function () { await makePutBodyRequest(options) }) - it('Should be able to login again') + it('Should be able to login again', async function () { + server.accessToken = await serverLogin(server) + }) it('Should have an expired access token') it('Should refresh the token') - it('Should be able to upload a video again') + it('Should be able to get my user information again', async function () { + await getMyUserInformation(server.url, server.accessToken) + }) }) 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 ({ peertubeHelpers }) { registerIdAndPassAuth({ - type: 'id-and-pass', + authName: 'spyro-auth', onLogout: () => { peertubeHelpers.logger.info('On logout for auth 1 - 1') @@ -16,7 +16,7 @@ async function register ({ return Promise.resolve({ username: 'spyro', email: 'spyro@example.com', - role: 0, + role: 2, displayName: 'Spyro the Dragon' }) } @@ -26,7 +26,7 @@ async function register ({ }) registerIdAndPassAuth({ - type: 'id-and-pass', + authName: 'crash-auth', onLogout: () => { peertubeHelpers.logger.info('On logout for auth 1 - 2') @@ -39,7 +39,7 @@ async function register ({ return Promise.resolve({ username: 'crash', email: 'crash@example.com', - role: 2, + role: 1, displayName: 'Crash Bandicoot' }) } 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 ({ peertubeHelpers }) { registerIdAndPassAuth({ - type: 'id-and-pass', + authName: 'laguna-bad-auth', onLogout: () => { 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 ({ peertubeHelpers }) { registerIdAndPassAuth({ - type: 'id-and-pass', + authName: 'laguna-auth', onLogout: () => { 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 @@ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ import 'mocha' -import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers' -import { getPluginTestPath, installPlugin, setAccessTokensToServers } from '../../../shared/extra-utils' +import { cleanupTests, flushAndRunServer, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers' +import { + getMyUserInformation, + getPluginTestPath, + installPlugin, + logout, + setAccessTokensToServers, + uninstallPlugin, + updateMyUser, + userLogin +} from '../../../shared/extra-utils' +import { User, UserRole } from '@shared/models' +import { expect } from 'chai' describe('Test id and pass auth plugins', function () { let server: ServerInfo + let crashToken: string before(async function () { this.timeout(30000) @@ -13,54 +25,97 @@ describe('Test id and pass auth plugins', function () { server = await flushAndRunServer(1) await setAccessTokensToServers([ server ]) - await installPlugin({ - url: server.url, - accessToken: server.accessToken, - path: getPluginTestPath('-id-pass-auth-one') - }) - - await installPlugin({ - url: server.url, - accessToken: server.accessToken, - path: getPluginTestPath('-id-pass-auth-two') - }) + for (const suffix of [ 'one', 'two', 'three' ]) { + await installPlugin({ + url: server.url, + accessToken: server.accessToken, + path: getPluginTestPath('-id-pass-auth-' + suffix) + }) + } }) - it('Should not login', async function() { - + it('Should not login', async function () { + await userLogin(server, { username: 'toto', password: 'password' }, 400) }) - it('Should login Spyro, create the user and use the token', async function() { + it('Should login Spyro, create the user and use the token', async function () { + const accessToken = await userLogin(server, { username: 'spyro', password: 'spyro password' }) + const res = await getMyUserInformation(server.url, accessToken) + + const body: User = res.body + expect(body.username).to.equal('spyro') + expect(body.account.displayName).to.equal('Spyro the Dragon') + expect(body.role).to.equal(UserRole.USER) }) - it('Should login Crash, create the user and use the token', async function() { + it('Should login Crash, create the user and use the token', async function () { + crashToken = await userLogin(server, { username: 'crash', password: 'crash password' }) + + const res = await getMyUserInformation(server.url, crashToken) + const body: User = res.body + expect(body.username).to.equal('crash') + expect(body.account.displayName).to.equal('Crash Bandicoot') + expect(body.role).to.equal(UserRole.MODERATOR) }) - it('Should login the first Laguna, create the user and use the token', async function() { + it('Should login the first Laguna, create the user and use the token', async function () { + const accessToken = await userLogin(server, { username: 'laguna', password: 'laguna password' }) + const res = await getMyUserInformation(server.url, accessToken) + + const body: User = res.body + expect(body.username).to.equal('laguna') + expect(body.account.displayName).to.equal('laguna') + expect(body.role).to.equal(UserRole.USER) }) it('Should update Crash profile', async function () { + await updateMyUser({ + url: server.url, + accessToken: crashToken, + displayName: 'Beautiful Crash', + description: 'Mutant eastern barred bandicoot' + }) + const res = await getMyUserInformation(server.url, crashToken) + + const body: User = res.body + expect(body.account.displayName).to.equal('Beautiful Crash') + expect(body.account.description).to.equal('Mutant eastern barred bandicoot') }) it('Should logout Crash', async function () { - - // test token + await logout(server.url, crashToken) }) - it('Should have logged the Crash logout', async function () { + it('Should have logged out Crash', async function () { + await getMyUserInformation(server.url, crashToken, 401) + await waitUntilLog(server, 'On logout for auth 1 - 2') }) it('Should login Crash and keep the old existing profile', async function () { + crashToken = await userLogin(server, { username: 'crash', password: 'crash password' }) + const res = await getMyUserInformation(server.url, crashToken) + + const body: User = res.body + expect(body.username).to.equal('crash') + expect(body.account.displayName).to.equal('Beautiful Crash') + expect(body.account.description).to.equal('Mutant eastern barred bandicoot') + expect(body.role).to.equal(UserRole.MODERATOR) }) it('Should uninstall the plugin one and do not login existing Crash', async function () { + await uninstallPlugin({ + url: server.url, + accessToken: server.accessToken, + npmName: 'peertube-plugin-test-id-pass-auth-one' + }) + await userLogin(server, { username: 'crash', password: 'crash password' }, 400) }) 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' { bypassLogin?: { bypass: boolean pluginName: string + authName?: string user: { username: string email: string @@ -45,6 +46,8 @@ declare module 'express' { } } + explicitLogout: boolean + videoAll?: MVideoFullLight onlyImmutableVideo?: MVideoImmutable 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' import { Router } from 'express' import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model' import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model' -import { RegisterServerAuthPassOptions, RegisterServerAuthExternalOptions, RegisterServerAuthExternalResult } from '@shared/models/plugins/register-server-auth.model' +import { + RegisterServerAuthExternalOptions, + RegisterServerAuthExternalResult, + RegisterServerAuthPassOptions +} from '@shared/models/plugins/register-server-auth.model' export type PeerTubeHelpers = { logger: Logger diff --git a/shared/models/plugins/register-server-auth.model.ts b/shared/models/plugins/register-server-auth.model.ts index 34ebbe712..dc46dcbc8 100644 --- a/shared/models/plugins/register-server-auth.model.ts +++ b/shared/models/plugins/register-server-auth.model.ts @@ -3,10 +3,12 @@ import { UserRole } from '@shared/models' export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions export interface RegisterServerAuthPassOptions { - type: 'id-and-pass' + // Authentication name (a plugin can register multiple auth strategies) + authName: string onLogout?: Function + // Weight of this authentication so PeerTube tries the auth methods in DESC weight order getWeight(): number // Used by PeerTube to login a user @@ -23,7 +25,8 @@ export interface RegisterServerAuthPassOptions { } export interface RegisterServerAuthExternalOptions { - type: 'external' + // Authentication name (a plugin can register multiple auth strategies) + authName: string onLogout?: Function } -- 2.25.1