From 2166c058f34dff6f91566930d12448805d829de7 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 7 Oct 2022 14:23:42 +0200 Subject: Allow admins to disable two factor auth --- .../users/user-edit/user-edit.component.html | 9 +- .../users/user-edit/user-edit.component.scss | 16 ++-- .../+admin/overview/users/user-edit/user-edit.ts | 12 +++ .../users/user-edit/user-update.component.ts | 21 ++++- .../my-account-two-factor/index.ts | 1 - .../my-account-two-factor-button.component.ts | 2 +- .../my-account-two-factor.component.ts | 2 +- .../my-account-two-factor/two-factor.service.ts | 52 ------------ client/src/app/+my-account/my-account.module.ts | 12 +-- client/src/app/shared/shared-users/index.ts | 1 + .../app/shared/shared-users/shared-users.module.ts | 4 +- .../app/shared/shared-users/two-factor.service.ts | 52 ++++++++++++ server/controllers/api/users/two-factor.ts | 6 +- server/helpers/peertube-crypto.ts | 2 + server/middlewares/validators/users.ts | 47 +++++++---- server/tests/api/check-params/two-factor.ts | 29 +++++-- server/tests/api/users/two-factor.ts | 95 ++++++++++++++++------ shared/server-commands/users/two-factor-command.ts | 21 ++++- 18 files changed, 252 insertions(+), 132 deletions(-) delete mode 100644 client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts create mode 100644 client/src/app/shared/shared-users/two-factor.service.ts diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.component.html b/client/src/app/+admin/overview/users/user-edit/user-edit.component.html index da5879a36..e51ccf808 100644 --- a/client/src/app/+admin/overview/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/overview/users/user-edit/user-edit.component.html @@ -204,7 +204,7 @@ -
+
@@ -213,7 +213,7 @@
-
+
@@ -222,6 +222,11 @@
+ +
+ + +
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss b/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss index 68fa1215f..698628149 100644 --- a/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss +++ b/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss @@ -48,17 +48,13 @@ my-user-real-quota-info { } .danger-zone { - .reset-password-email { - margin-bottom: 30px; + button { + @include peertube-button; + @include danger-button; + @include disable-outline; - button { - @include peertube-button; - @include danger-button; - @include disable-outline; - - display: block; - margin-top: 0; - } + display: block; + margin-top: 0; } } diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.ts b/client/src/app/+admin/overview/users/user-edit/user-edit.ts index 6dae4110d..21e9629ab 100644 --- a/client/src/app/+admin/overview/users/user-edit/user-edit.ts +++ b/client/src/app/+admin/overview/users/user-edit/user-edit.ts @@ -60,10 +60,22 @@ export abstract class UserEdit extends FormReactive implements OnInit { ] } + displayDangerZone () { + if (this.isCreation()) return false + if (this.user?.pluginAuth) return false + if (this.auth.getUser().id === this.user.id) return false + + return true + } + resetPassword () { return } + disableTwoFactorAuth () { + return + } + getUserVideoQuota () { return this.form.value['videoQuota'] } diff --git a/client/src/app/+admin/overview/users/user-edit/user-update.component.ts b/client/src/app/+admin/overview/users/user-edit/user-update.component.ts index bab288a67..1482a1902 100644 --- a/client/src/app/+admin/overview/users/user-edit/user-update.component.ts +++ b/client/src/app/+admin/overview/users/user-edit/user-update.component.ts @@ -10,7 +10,7 @@ import { USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators' import { FormValidatorService } from '@app/shared/shared-forms' -import { UserAdminService } from '@app/shared/shared-users' +import { TwoFactorService, UserAdminService } from '@app/shared/shared-users' import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models' import { UserEdit } from './user-edit' @@ -34,6 +34,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { private router: Router, private notifier: Notifier, private userService: UserService, + private twoFactorService: TwoFactorService, private userAdminService: UserAdminService ) { super() @@ -120,10 +121,22 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { this.notifier.success($localize`An email asking for password reset has been sent to ${this.user.username}.`) }, - error: err => { - this.error = err.message - } + error: err => this.notifier.error(err.message) + }) + } + + disableTwoFactorAuth () { + this.twoFactorService.disableTwoFactor({ userId: this.user.id }) + .subscribe({ + next: () => { + this.user.twoFactorEnabled = false + + this.notifier.success($localize`Two factor authentication of ${this.user.username} disabled.`) + }, + + error: err => this.notifier.error(err.message) }) + } private onUserFetched (userJson: UserType) { diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts index ef83009a5..cc774bde3 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts @@ -1,3 +1,2 @@ export * from './my-account-two-factor-button.component' export * from './my-account-two-factor.component' -export * from './two-factor.service' diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts index 03b00e933..97ffb6013 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts @@ -1,7 +1,7 @@ import { Subject } from 'rxjs' import { Component, Input, OnInit } from '@angular/core' import { AuthService, ConfirmService, Notifier, User } from '@app/core' -import { TwoFactorService } from './two-factor.service' +import { TwoFactorService } from '@app/shared/shared-users' @Component({ selector: 'my-account-two-factor-button', diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts index e4d4188f7..259090d64 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts @@ -4,7 +4,7 @@ import { Router } from '@angular/router' import { AuthService, Notifier, User } from '@app/core' import { USER_EXISTING_PASSWORD_VALIDATOR, USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-validators' import { FormReactiveService } from '@app/shared/shared-forms' -import { TwoFactorService } from './two-factor.service' +import { TwoFactorService } from '@app/shared/shared-users' @Component({ selector: 'my-account-two-factor', diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts deleted file mode 100644 index c0e5ac492..000000000 --- a/client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { catchError } from 'rxjs/operators' -import { HttpClient } from '@angular/common/http' -import { Injectable } from '@angular/core' -import { RestExtractor, UserService } from '@app/core' -import { TwoFactorEnableResult } from '@shared/models' - -@Injectable() -export class TwoFactorService { - constructor ( - private authHttp: HttpClient, - private restExtractor: RestExtractor - ) { } - - // --------------------------------------------------------------------------- - - requestTwoFactor (options: { - userId: number - currentPassword: string - }) { - const { userId, currentPassword } = options - - const url = UserService.BASE_USERS_URL + userId + '/two-factor/request' - - return this.authHttp.post(url, { currentPassword }) - .pipe(catchError(err => this.restExtractor.handleError(err))) - } - - confirmTwoFactorRequest (options: { - userId: number - requestToken: string - otpToken: string - }) { - const { userId, requestToken, otpToken } = options - - const url = UserService.BASE_USERS_URL + userId + '/two-factor/confirm-request' - - return this.authHttp.post(url, { requestToken, otpToken }) - .pipe(catchError(err => this.restExtractor.handleError(err))) - } - - disableTwoFactor (options: { - userId: number - currentPassword: string - }) { - const { userId, currentPassword } = options - - const url = UserService.BASE_USERS_URL + userId + '/two-factor/disable' - - return this.authHttp.post(url, { currentPassword }) - .pipe(catchError(err => this.restExtractor.handleError(err))) - } -} diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index f5beaa4db..84b057647 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts @@ -11,6 +11,7 @@ import { SharedMainModule } from '@app/shared/shared-main' import { SharedModerationModule } from '@app/shared/shared-moderation' import { SharedShareModal } from '@app/shared/shared-share-modal' import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' +import { SharedUsersModule } from '@app/shared/shared-users' import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module' import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' @@ -24,11 +25,7 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' -import { - MyAccountTwoFactorButtonComponent, - MyAccountTwoFactorComponent, - TwoFactorService -} from './my-account-settings/my-account-two-factor' +import { MyAccountTwoFactorButtonComponent, MyAccountTwoFactorComponent } from './my-account-settings/my-account-two-factor' import { MyAccountComponent } from './my-account.component' @NgModule({ @@ -44,6 +41,7 @@ import { MyAccountComponent } from './my-account.component' SharedFormModule, SharedModerationModule, SharedUserInterfaceSettingsModule, + SharedUsersModule, SharedGlobalIconModule, SharedAbuseListModule, SharedShareModal, @@ -74,9 +72,7 @@ import { MyAccountComponent } from './my-account.component' MyAccountComponent ], - providers: [ - TwoFactorService - ] + providers: [] }) export class MyAccountModule { } diff --git a/client/src/app/shared/shared-users/index.ts b/client/src/app/shared/shared-users/index.ts index 8f90f2515..20e60486d 100644 --- a/client/src/app/shared/shared-users/index.ts +++ b/client/src/app/shared/shared-users/index.ts @@ -1,4 +1,5 @@ export * from './user-admin.service' export * from './user-signup.service' +export * from './two-factor.service' export * from './shared-users.module' diff --git a/client/src/app/shared/shared-users/shared-users.module.ts b/client/src/app/shared/shared-users/shared-users.module.ts index 2a1dadf20..5a1675dc9 100644 --- a/client/src/app/shared/shared-users/shared-users.module.ts +++ b/client/src/app/shared/shared-users/shared-users.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core' import { SharedMainModule } from '../shared-main/shared-main.module' +import { TwoFactorService } from './two-factor.service' import { UserAdminService } from './user-admin.service' import { UserSignupService } from './user-signup.service' @@ -15,7 +16,8 @@ import { UserSignupService } from './user-signup.service' providers: [ UserSignupService, - UserAdminService + UserAdminService, + TwoFactorService ] }) export class SharedUsersModule { } diff --git a/client/src/app/shared/shared-users/two-factor.service.ts b/client/src/app/shared/shared-users/two-factor.service.ts new file mode 100644 index 000000000..9ff916f15 --- /dev/null +++ b/client/src/app/shared/shared-users/two-factor.service.ts @@ -0,0 +1,52 @@ +import { catchError } from 'rxjs/operators' +import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor, UserService } from '@app/core' +import { TwoFactorEnableResult } from '@shared/models' + +@Injectable() +export class TwoFactorService { + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor + ) { } + + // --------------------------------------------------------------------------- + + requestTwoFactor (options: { + userId: number + currentPassword: string + }) { + const { userId, currentPassword } = options + + const url = UserService.BASE_USERS_URL + userId + '/two-factor/request' + + return this.authHttp.post(url, { currentPassword }) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + confirmTwoFactorRequest (options: { + userId: number + requestToken: string + otpToken: string + }) { + const { userId, requestToken, otpToken } = options + + const url = UserService.BASE_USERS_URL + userId + '/two-factor/confirm-request' + + return this.authHttp.post(url, { requestToken, otpToken }) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + disableTwoFactor (options: { + userId: number + currentPassword?: string + }) { + const { userId, currentPassword } = options + + const url = UserService.BASE_USERS_URL + userId + '/two-factor/disable' + + return this.authHttp.post(url, { currentPassword }) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } +} diff --git a/server/controllers/api/users/two-factor.ts b/server/controllers/api/users/two-factor.ts index 1725294e7..79f63a62d 100644 --- a/server/controllers/api/users/two-factor.ts +++ b/server/controllers/api/users/two-factor.ts @@ -1,7 +1,7 @@ import express from 'express' import { generateOTPSecret, isOTPValid } from '@server/helpers/otp' import { Redis } from '@server/lib/redis' -import { asyncMiddleware, authenticate, usersCheckCurrentPassword } from '@server/middlewares' +import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares' import { confirmTwoFactorValidator, disableTwoFactorValidator, @@ -13,7 +13,7 @@ const twoFactorRouter = express.Router() twoFactorRouter.post('/:id/two-factor/request', authenticate, - asyncMiddleware(usersCheckCurrentPassword), + asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)), asyncMiddleware(requestOrConfirmTwoFactorValidator), asyncMiddleware(requestTwoFactor) ) @@ -27,7 +27,7 @@ twoFactorRouter.post('/:id/two-factor/confirm-request', twoFactorRouter.post('/:id/two-factor/disable', authenticate, - asyncMiddleware(usersCheckCurrentPassword), + asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)), asyncMiddleware(disableTwoFactorValidator), asyncMiddleware(disableTwoFactor) ) diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 8aca50900..dcf47ce76 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts @@ -24,6 +24,8 @@ function createPrivateAndPublicKeys () { // User password checks function comparePassword (plainPassword: string, hashPassword: string) { + if (!plainPassword) return Promise.resolve(false) + return bcryptComparePromise(plainPassword, hashPassword) } diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 046029547..055af3b64 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -506,23 +506,40 @@ const usersVerifyEmailValidator = [ } ] -const usersCheckCurrentPassword = [ - body('currentPassword').custom(exists), +const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => { + return [ + body('currentPassword').optional().custom(exists), - async (req: express.Request, res: express.Response, next: express.NextFunction) => { - if (areValidationErrors(req, res)) return + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + if (areValidationErrors(req, res)) return - const user = res.locals.oauth.token.User - if (await user.isPasswordMatch(req.body.currentPassword) !== true) { - return res.fail({ - status: HttpStatusCode.FORBIDDEN_403, - message: 'currentPassword is invalid.' - }) - } + const user = res.locals.oauth.token.User + const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR + const targetUserId = parseInt(targetUserIdGetter(req) + '') - return next() - } -] + // Admin/moderator action on another user, skip the password check + if (isAdminOrModerator && targetUserId !== user.id) { + return next() + } + + if (!req.body.currentPassword) { + return res.fail({ + status: HttpStatusCode.BAD_REQUEST_400, + message: 'currentPassword is missing' + }) + } + + if (await user.isPasswordMatch(req.body.currentPassword) !== true) { + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'currentPassword is invalid.' + }) + } + + return next() + } + ] +} const userAutocompleteValidator = [ param('search') @@ -591,7 +608,7 @@ export { usersUpdateValidator, usersUpdateMeValidator, usersVideoRatingValidator, - usersCheckCurrentPassword, + usersCheckCurrentPasswordFactory, ensureUserRegistrationAllowed, ensureUserRegistrationAllowedForIP, usersGetValidator, diff --git a/server/tests/api/check-params/two-factor.ts b/server/tests/api/check-params/two-factor.ts index e7ca5490c..f8365f1b5 100644 --- a/server/tests/api/check-params/two-factor.ts +++ b/server/tests/api/check-params/two-factor.ts @@ -86,6 +86,15 @@ describe('Test two factor API validators', function () { }) }) + it('Should succeed to request two factor without a password when targeting a remote user with an admin account', async function () { + await server.twoFactor.request({ userId }) + }) + + it('Should fail to request two factor without a password when targeting myself with an admin account', async function () { + await server.twoFactor.request({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.twoFactor.request({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + it('Should succeed to request my two factor auth', async function () { { const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword }) @@ -234,7 +243,7 @@ describe('Test two factor API validators', function () { }) }) - it('Should fail to disabled two factor with an incorrect password', async function () { + it('Should fail to disable two factor with an incorrect password', async function () { await server.twoFactor.disable({ userId, token: userToken, @@ -243,16 +252,20 @@ describe('Test two factor API validators', function () { }) }) + it('Should succeed to disable two factor without a password when targeting a remote user with an admin account', async function () { + await server.twoFactor.disable({ userId }) + await server.twoFactor.requestAndConfirm({ userId }) + }) + + it('Should fail to disable two factor without a password when targeting myself with an admin account', async function () { + await server.twoFactor.disable({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.twoFactor.disable({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + it('Should succeed to disable another user two factor with the appropriate rights', async function () { await server.twoFactor.disable({ userId, currentPassword: rootPassword }) - // Reinit - const { otpRequest } = await server.twoFactor.request({ userId, currentPassword: rootPassword }) - await server.twoFactor.confirmRequest({ - userId, - requestToken: otpRequest.requestToken, - otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate() - }) + await server.twoFactor.requestAndConfirm({ userId }) }) it('Should succeed to update my two factor auth', async function () { diff --git a/server/tests/api/users/two-factor.ts b/server/tests/api/users/two-factor.ts index 450aac4dc..0dcab9e17 100644 --- a/server/tests/api/users/two-factor.ts +++ b/server/tests/api/users/two-factor.ts @@ -7,13 +7,14 @@ import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServ async function login (options: { server: PeerTubeServer - password?: string + username: string + password: string otpToken?: string expectedStatus?: HttpStatusCode }) { - const { server, password = server.store.user.password, otpToken, expectedStatus } = options + const { server, username, password, otpToken, expectedStatus } = options - const user = { username: server.store.user.username, password } + const user = { username, password } const { res, body: { access_token: token } } = await server.login.loginAndGetResponse({ user, otpToken, expectedStatus }) return { res, token } @@ -21,23 +22,28 @@ async function login (options: { describe('Test users', function () { let server: PeerTubeServer - let rootId: number let otpSecret: string let requestToken: string + const userUsername = 'user1' + let userId: number + let userPassword: string + let userToken: string + before(async function () { this.timeout(30000) server = await createSingleServer(1) await setAccessTokensToServers([ server ]) - - const { id } = await server.users.getMyInfo() - rootId = id + const res = await server.users.generate(userUsername) + userId = res.userId + userPassword = res.password + userToken = res.token }) it('Should not add the header on login if two factor is not enabled', async function () { - const { res, token } = await login({ server }) + const { res, token } = await login({ server, username: userUsername, password: userPassword }) expect(res.header['x-peertube-otp']).to.not.exist @@ -45,10 +51,7 @@ describe('Test users', function () { }) it('Should request two factor and get the secret and uri', async function () { - const { otpRequest } = await server.twoFactor.request({ - userId: rootId, - currentPassword: server.store.user.password - }) + const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword }) expect(otpRequest.requestToken).to.exist @@ -64,27 +67,33 @@ describe('Test users', function () { }) it('Should not have two factor confirmed yet', async function () { - const { twoFactorEnabled } = await server.users.getMyInfo() + const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken }) expect(twoFactorEnabled).to.be.false }) it('Should confirm two factor', async function () { await server.twoFactor.confirmRequest({ - userId: rootId, + userId, + token: userToken, otpToken: TwoFactorCommand.buildOTP({ secret: otpSecret }).generate(), requestToken }) }) it('Should not add the header on login if two factor is enabled and password is incorrect', async function () { - const { res, token } = await login({ server, password: 'fake', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + const { res, token } = await login({ server, username: userUsername, password: 'fake', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) expect(res.header['x-peertube-otp']).to.not.exist expect(token).to.not.exist }) it('Should add the header on login if two factor is enabled and password is correct', async function () { - const { res, token } = await login({ server, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + const { res, token } = await login({ + server, + username: userUsername, + password: userPassword, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) expect(res.header['x-peertube-otp']).to.exist expect(token).to.not.exist @@ -95,14 +104,26 @@ describe('Test users', function () { it('Should not login with correct password and incorrect otp secret', async function () { const otp = TwoFactorCommand.buildOTP({ secret: 'a'.repeat(32) }) - const { res, token } = await login({ server, otpToken: otp.generate(), expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + const { res, token } = await login({ + server, + username: userUsername, + password: userPassword, + otpToken: otp.generate(), + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) expect(res.header['x-peertube-otp']).to.not.exist expect(token).to.not.exist }) it('Should not login with correct password and incorrect otp code', async function () { - const { res, token } = await login({ server, otpToken: '123456', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + const { res, token } = await login({ + server, + username: userUsername, + password: userPassword, + otpToken: '123456', + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) expect(res.header['x-peertube-otp']).to.not.exist expect(token).to.not.exist @@ -111,7 +132,13 @@ describe('Test users', function () { it('Should not login with incorrect password and correct otp code', async function () { const otpToken = TwoFactorCommand.buildOTP({ secret: otpSecret }).generate() - const { res, token } = await login({ server, password: 'fake', otpToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + const { res, token } = await login({ + server, + username: userUsername, + password: 'fake', + otpToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) expect(res.header['x-peertube-otp']).to.not.exist expect(token).to.not.exist @@ -120,7 +147,7 @@ describe('Test users', function () { it('Should correctly login with correct password and otp code', async function () { const otpToken = TwoFactorCommand.buildOTP({ secret: otpSecret }).generate() - const { res, token } = await login({ server, otpToken }) + const { res, token } = await login({ server, username: userUsername, password: userPassword, otpToken }) expect(res.header['x-peertube-otp']).to.not.exist expect(token).to.exist @@ -129,21 +156,41 @@ describe('Test users', function () { }) it('Should have two factor enabled when getting my info', async function () { - const { twoFactorEnabled } = await server.users.getMyInfo() + const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken }) expect(twoFactorEnabled).to.be.true }) it('Should disable two factor and be able to login without otp token', async function () { - await server.twoFactor.disable({ userId: rootId, currentPassword: server.store.user.password }) + await server.twoFactor.disable({ userId, token: userToken, currentPassword: userPassword }) - const { res, token } = await login({ server }) + const { res, token } = await login({ server, username: userUsername, password: userPassword }) expect(res.header['x-peertube-otp']).to.not.exist await server.users.getMyInfo({ token }) }) it('Should have two factor disabled when getting my info', async function () { - const { twoFactorEnabled } = await server.users.getMyInfo() + const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken }) + expect(twoFactorEnabled).to.be.false + }) + + it('Should enable two factor auth without password from an admin', async function () { + const { otpRequest } = await server.twoFactor.request({ userId }) + + await server.twoFactor.confirmRequest({ + userId, + otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate(), + requestToken: otpRequest.requestToken + }) + + const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken }) + expect(twoFactorEnabled).to.be.true + }) + + it('Should disable two factor auth without password from an admin', async function () { + await server.twoFactor.disable({ userId }) + + const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken }) expect(twoFactorEnabled).to.be.false }) diff --git a/shared/server-commands/users/two-factor-command.ts b/shared/server-commands/users/two-factor-command.ts index 6c9d270ae..5542acfda 100644 --- a/shared/server-commands/users/two-factor-command.ts +++ b/shared/server-commands/users/two-factor-command.ts @@ -21,7 +21,7 @@ export class TwoFactorCommand extends AbstractCommand { request (options: OverrideCommandOptions & { userId: number - currentPassword: string + currentPassword?: string }) { const { currentPassword, userId } = options @@ -58,7 +58,7 @@ export class TwoFactorCommand extends AbstractCommand { disable (options: OverrideCommandOptions & { userId: number - currentPassword: string + currentPassword?: string }) { const { userId, currentPassword } = options const path = '/api/v1/users/' + userId + '/two-factor/disable' @@ -72,4 +72,21 @@ export class TwoFactorCommand extends AbstractCommand { defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 }) } + + async requestAndConfirm (options: OverrideCommandOptions & { + userId: number + currentPassword?: string + }) { + const { userId, currentPassword } = options + + const { otpRequest } = await this.request({ userId, currentPassword }) + + await this.confirmRequest({ + userId, + requestToken: otpRequest.requestToken, + otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate() + }) + + return otpRequest + } } -- cgit v1.2.3