]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Allow admins to disable two factor auth
authorChocobozzz <me@florianbigard.com>
Fri, 7 Oct 2022 12:23:42 +0000 (14:23 +0200)
committerChocobozzz <me@florianbigard.com>
Fri, 7 Oct 2022 12:28:35 +0000 (14:28 +0200)
17 files changed:
client/src/app/+admin/overview/users/user-edit/user-edit.component.html
client/src/app/+admin/overview/users/user-edit/user-edit.component.scss
client/src/app/+admin/overview/users/user-edit/user-edit.ts
client/src/app/+admin/overview/users/user-edit/user-update.component.ts
client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts
client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts
client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts
client/src/app/+my-account/my-account.module.ts
client/src/app/shared/shared-users/index.ts
client/src/app/shared/shared-users/shared-users.module.ts
client/src/app/shared/shared-users/two-factor.service.ts [moved from client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts with 98% similarity]
server/controllers/api/users/two-factor.ts
server/helpers/peertube-crypto.ts
server/middlewares/validators/users.ts
server/tests/api/check-params/two-factor.ts
server/tests/api/users/two-factor.ts
shared/server-commands/users/two-factor-command.ts

index da5879a36fb1ae1850d9a2b95d67d0519cb021ce..e51ccf80847075e8292707ac731a8f3b4a4706fa 100644 (file)
 </div>
 
 
-<div *ngIf="!isCreation() && user && user.pluginAuth === null" class="row mt-4"> <!-- danger zone grid -->
+<div *ngIf="displayDangerZone()" class="row mt-4"> <!-- danger zone grid -->
   <div class="col-12 col-lg-4 col-xl-3">
     <div class="anchor" id="danger"></div> <!-- danger zone anchor -->
     <div i18n class="account-title account-title-danger">DANGER ZONE</div>
   <div class="col-12 col-lg-8 col-xl-9">
 
     <div class="danger-zone">
-      <div class="form-group reset-password-email">
+      <div class="form-group">
         <label i18n>Send a link to reset the password by email to the user</label>
         <button (click)="resetPassword()" i18n>Ask for new password</button>
       </div>
         <label i18n>Manually set the user password</label>
         <my-user-password [userId]="user.id"></my-user-password>
       </div>
+
+      <div *ngIf="user.twoFactorEnabled" class="form-group">
+        <label i18n>This user has two factor authentication enabled</label>
+        <button (click)="disableTwoFactorAuth()" i18n>Disable two factor authentication</button>
+      </div>
     </div>
 
   </div>
index 68fa1215fccc15cc9f0686c4246bde7bd3ed0669..6986281497f3d64048f9f482482606beb48e76bd 100644 (file)
@@ -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;
   }
 }
 
index 6dae4110d43ef00baab89c243cdcc896bda887d3..21e9629aba71e45d2524c16a27ec3b5308beae5b 100644 (file)
@@ -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']
   }
index bab288a6760c309480cb69eb5110c9d5465a4709..1482a190276f47b0a5fba8f2c63512410ff54164 100644 (file)
@@ -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) {
index ef83009a51b8514581e1e579075d4fb4482b3f6e..cc774bde33c1115f460cef68aed0a3c0039206c4 100644 (file)
@@ -1,3 +1,2 @@
 export * from './my-account-two-factor-button.component'
 export * from './my-account-two-factor.component'
-export * from './two-factor.service'
index 03b00e9335bf4bc799604ade6b7de5bc5c619b66..97ffb6013492eefb5969c71152bc3328feab40ef 100644 (file)
@@ -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',
index e4d4188f7dc86d9704c640cb0d9802b91d7ecd62..259090d644fa37290686df87873990329350b9ff 100644 (file)
@@ -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',
index f5beaa4db1238ab54806a8f3f44923eb9159e600..84b05764709e4c595f872a44e9d1308f445d2974 100644 (file)
@@ -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 {
 }
index 8f90f251598f0127924666d6f912c7110b8129d4..20e60486dae5c8d563490c228bf7efd1bcbb7891 100644 (file)
@@ -1,4 +1,5 @@
 export * from './user-admin.service'
 export * from './user-signup.service'
+export * from './two-factor.service'
 
 export * from './shared-users.module'
index 2a1dadf20f2bbc10663f3f3315a03409aaff19d4..5a1675dc94c25565bf0a19f4116503353adaf376 100644 (file)
@@ -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 { }
similarity index 98%
rename from client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts
rename to client/src/app/shared/shared-users/two-factor.service.ts
index c0e5ac49271073d2e054b3e192b96d38ce04e30f..9ff916f1540a36f8b2d8ab64e1d9a6169e3da5a6 100644 (file)
@@ -40,7 +40,7 @@ export class TwoFactorService {
 
   disableTwoFactor (options: {
     userId: number
-    currentPassword: string
+    currentPassword?: string
   }) {
     const { userId, currentPassword } = options
 
index 1725294e78e479d42f2b96c04eff0f8c4934a10b..79f63a62d4e971680265ffd0f53bd2d4168f1e6e 100644 (file)
@@ -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)
 )
index 8aca509009bb1649aefce6fe7abdc9046e67a9a2..dcf47ce761efb5752cb2af92c0fb7283a9a62cf4 100644 (file)
@@ -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)
 }
 
index 0460295478c372d20d1059e32f4ad7537310b21d..055af3b64c851209ecd24c97614c6c1ca3175d86 100644 (file)
@@ -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,
index e7ca5490cee906fe924e56ccdd2acf3a7f2c26c9..f8365f1b567b5103630ab88cf7bef68d140b7356 100644 (file)
@@ -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 () {
index 450aac4dc8a8253b8e0275f2df15b7dfaf55255b..0dcab9e17cd9c4ab0d3adcb98db3a67a966fc78d 100644 (file)
@@ -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
   })
 
index 6c9d270aed8309fcd70c62629968d132a3a309a2..5542acfdad994a99883e4431ff42bdd73353da33 100644 (file)
@@ -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
+  }
 }