aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-10-07 14:23:42 +0200
committerChocobozzz <me@florianbigard.com>2022-10-07 14:28:35 +0200
commit2166c058f34dff6f91566930d12448805d829de7 (patch)
tree2b9100b8eccbac287d1105c765901f966a354986
parentd12b40fb96d56786a96c06a621f3d8e0a0d24f4a (diff)
downloadPeerTube-2166c058f34dff6f91566930d12448805d829de7.tar.gz
PeerTube-2166c058f34dff6f91566930d12448805d829de7.tar.zst
PeerTube-2166c058f34dff6f91566930d12448805d829de7.zip
Allow admins to disable two factor auth
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-edit.component.html9
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-edit.component.scss16
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-edit.ts12
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-update.component.ts21
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts1
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts2
-rw-r--r--client/src/app/+my-account/my-account.module.ts12
-rw-r--r--client/src/app/shared/shared-users/index.ts1
-rw-r--r--client/src/app/shared/shared-users/shared-users.module.ts4
-rw-r--r--client/src/app/shared/shared-users/two-factor.service.ts (renamed from client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts)2
-rw-r--r--server/controllers/api/users/two-factor.ts6
-rw-r--r--server/helpers/peertube-crypto.ts2
-rw-r--r--server/middlewares/validators/users.ts47
-rw-r--r--server/tests/api/check-params/two-factor.ts29
-rw-r--r--server/tests/api/users/two-factor.ts95
-rw-r--r--shared/server-commands/users/two-factor-command.ts21
17 files changed, 201 insertions, 81 deletions
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 @@
204</div> 204</div>
205 205
206 206
207<div *ngIf="!isCreation() && user && user.pluginAuth === null" class="row mt-4"> <!-- danger zone grid --> 207<div *ngIf="displayDangerZone()" class="row mt-4"> <!-- danger zone grid -->
208 <div class="col-12 col-lg-4 col-xl-3"> 208 <div class="col-12 col-lg-4 col-xl-3">
209 <div class="anchor" id="danger"></div> <!-- danger zone anchor --> 209 <div class="anchor" id="danger"></div> <!-- danger zone anchor -->
210 <div i18n class="account-title account-title-danger">DANGER ZONE</div> 210 <div i18n class="account-title account-title-danger">DANGER ZONE</div>
@@ -213,7 +213,7 @@
213 <div class="col-12 col-lg-8 col-xl-9"> 213 <div class="col-12 col-lg-8 col-xl-9">
214 214
215 <div class="danger-zone"> 215 <div class="danger-zone">
216 <div class="form-group reset-password-email"> 216 <div class="form-group">
217 <label i18n>Send a link to reset the password by email to the user</label> 217 <label i18n>Send a link to reset the password by email to the user</label>
218 <button (click)="resetPassword()" i18n>Ask for new password</button> 218 <button (click)="resetPassword()" i18n>Ask for new password</button>
219 </div> 219 </div>
@@ -222,6 +222,11 @@
222 <label i18n>Manually set the user password</label> 222 <label i18n>Manually set the user password</label>
223 <my-user-password [userId]="user.id"></my-user-password> 223 <my-user-password [userId]="user.id"></my-user-password>
224 </div> 224 </div>
225
226 <div *ngIf="user.twoFactorEnabled" class="form-group">
227 <label i18n>This user has two factor authentication enabled</label>
228 <button (click)="disableTwoFactorAuth()" i18n>Disable two factor authentication</button>
229 </div>
225 </div> 230 </div>
226 231
227 </div> 232 </div>
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 {
48} 48}
49 49
50.danger-zone { 50.danger-zone {
51 .reset-password-email { 51 button {
52 margin-bottom: 30px; 52 @include peertube-button;
53 @include danger-button;
54 @include disable-outline;
53 55
54 button { 56 display: block;
55 @include peertube-button; 57 margin-top: 0;
56 @include danger-button;
57 @include disable-outline;
58
59 display: block;
60 margin-top: 0;
61 }
62 } 58 }
63} 59}
64 60
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 {
60 ] 60 ]
61 } 61 }
62 62
63 displayDangerZone () {
64 if (this.isCreation()) return false
65 if (this.user?.pluginAuth) return false
66 if (this.auth.getUser().id === this.user.id) return false
67
68 return true
69 }
70
63 resetPassword () { 71 resetPassword () {
64 return 72 return
65 } 73 }
66 74
75 disableTwoFactorAuth () {
76 return
77 }
78
67 getUserVideoQuota () { 79 getUserVideoQuota () {
68 return this.form.value['videoQuota'] 80 return this.form.value['videoQuota']
69 } 81 }
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 {
10 USER_VIDEO_QUOTA_VALIDATOR 10 USER_VIDEO_QUOTA_VALIDATOR
11} from '@app/shared/form-validators/user-validators' 11} from '@app/shared/form-validators/user-validators'
12import { FormValidatorService } from '@app/shared/shared-forms' 12import { FormValidatorService } from '@app/shared/shared-forms'
13import { UserAdminService } from '@app/shared/shared-users' 13import { TwoFactorService, UserAdminService } from '@app/shared/shared-users'
14import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models' 14import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models'
15import { UserEdit } from './user-edit' 15import { UserEdit } from './user-edit'
16 16
@@ -34,6 +34,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
34 private router: Router, 34 private router: Router,
35 private notifier: Notifier, 35 private notifier: Notifier,
36 private userService: UserService, 36 private userService: UserService,
37 private twoFactorService: TwoFactorService,
37 private userAdminService: UserAdminService 38 private userAdminService: UserAdminService
38 ) { 39 ) {
39 super() 40 super()
@@ -120,10 +121,22 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
120 this.notifier.success($localize`An email asking for password reset has been sent to ${this.user.username}.`) 121 this.notifier.success($localize`An email asking for password reset has been sent to ${this.user.username}.`)
121 }, 122 },
122 123
123 error: err => { 124 error: err => this.notifier.error(err.message)
124 this.error = err.message 125 })
125 } 126 }
127
128 disableTwoFactorAuth () {
129 this.twoFactorService.disableTwoFactor({ userId: this.user.id })
130 .subscribe({
131 next: () => {
132 this.user.twoFactorEnabled = false
133
134 this.notifier.success($localize`Two factor authentication of ${this.user.username} disabled.`)
135 },
136
137 error: err => this.notifier.error(err.message)
126 }) 138 })
139
127 } 140 }
128 141
129 private onUserFetched (userJson: UserType) { 142 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 @@
1export * from './my-account-two-factor-button.component' 1export * from './my-account-two-factor-button.component'
2export * from './my-account-two-factor.component' 2export * from './my-account-two-factor.component'
3export * 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 @@
1import { Subject } from 'rxjs' 1import { Subject } from 'rxjs'
2import { Component, Input, OnInit } from '@angular/core' 2import { Component, Input, OnInit } from '@angular/core'
3import { AuthService, ConfirmService, Notifier, User } from '@app/core' 3import { AuthService, ConfirmService, Notifier, User } from '@app/core'
4import { TwoFactorService } from './two-factor.service' 4import { TwoFactorService } from '@app/shared/shared-users'
5 5
6@Component({ 6@Component({
7 selector: 'my-account-two-factor-button', 7 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'
4import { AuthService, Notifier, User } from '@app/core' 4import { AuthService, Notifier, User } from '@app/core'
5import { USER_EXISTING_PASSWORD_VALIDATOR, USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-validators' 5import { USER_EXISTING_PASSWORD_VALIDATOR, USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-validators'
6import { FormReactiveService } from '@app/shared/shared-forms' 6import { FormReactiveService } from '@app/shared/shared-forms'
7import { TwoFactorService } from './two-factor.service' 7import { TwoFactorService } from '@app/shared/shared-users'
8 8
9@Component({ 9@Component({
10 selector: 'my-account-two-factor', 10 selector: 'my-account-two-factor',
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'
11import { SharedModerationModule } from '@app/shared/shared-moderation' 11import { SharedModerationModule } from '@app/shared/shared-moderation'
12import { SharedShareModal } from '@app/shared/shared-share-modal' 12import { SharedShareModal } from '@app/shared/shared-share-modal'
13import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' 13import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings'
14import { SharedUsersModule } from '@app/shared/shared-users'
14import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module' 15import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
15import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' 16import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
16import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' 17import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
@@ -24,11 +25,7 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d
24import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' 25import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences'
25import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' 26import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component'
26import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' 27import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
27import { 28import { MyAccountTwoFactorButtonComponent, MyAccountTwoFactorComponent } from './my-account-settings/my-account-two-factor'
28 MyAccountTwoFactorButtonComponent,
29 MyAccountTwoFactorComponent,
30 TwoFactorService
31} from './my-account-settings/my-account-two-factor'
32import { MyAccountComponent } from './my-account.component' 29import { MyAccountComponent } from './my-account.component'
33 30
34@NgModule({ 31@NgModule({
@@ -44,6 +41,7 @@ import { MyAccountComponent } from './my-account.component'
44 SharedFormModule, 41 SharedFormModule,
45 SharedModerationModule, 42 SharedModerationModule,
46 SharedUserInterfaceSettingsModule, 43 SharedUserInterfaceSettingsModule,
44 SharedUsersModule,
47 SharedGlobalIconModule, 45 SharedGlobalIconModule,
48 SharedAbuseListModule, 46 SharedAbuseListModule,
49 SharedShareModal, 47 SharedShareModal,
@@ -74,9 +72,7 @@ import { MyAccountComponent } from './my-account.component'
74 MyAccountComponent 72 MyAccountComponent
75 ], 73 ],
76 74
77 providers: [ 75 providers: []
78 TwoFactorService
79 ]
80}) 76})
81export class MyAccountModule { 77export class MyAccountModule {
82} 78}
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 @@
1export * from './user-admin.service' 1export * from './user-admin.service'
2export * from './user-signup.service' 2export * from './user-signup.service'
3export * from './two-factor.service'
3 4
4export * from './shared-users.module' 5export * 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 @@
1 1
2import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
3import { SharedMainModule } from '../shared-main/shared-main.module' 3import { SharedMainModule } from '../shared-main/shared-main.module'
4import { TwoFactorService } from './two-factor.service'
4import { UserAdminService } from './user-admin.service' 5import { UserAdminService } from './user-admin.service'
5import { UserSignupService } from './user-signup.service' 6import { UserSignupService } from './user-signup.service'
6 7
@@ -15,7 +16,8 @@ import { UserSignupService } from './user-signup.service'
15 16
16 providers: [ 17 providers: [
17 UserSignupService, 18 UserSignupService,
18 UserAdminService 19 UserAdminService,
20 TwoFactorService
19 ] 21 ]
20}) 22})
21export class SharedUsersModule { } 23export class SharedUsersModule { }
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts b/client/src/app/shared/shared-users/two-factor.service.ts
index c0e5ac492..9ff916f15 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts
+++ b/client/src/app/shared/shared-users/two-factor.service.ts
@@ -40,7 +40,7 @@ export class TwoFactorService {
40 40
41 disableTwoFactor (options: { 41 disableTwoFactor (options: {
42 userId: number 42 userId: number
43 currentPassword: string 43 currentPassword?: string
44 }) { 44 }) {
45 const { userId, currentPassword } = options 45 const { userId, currentPassword } = options
46 46
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 @@
1import express from 'express' 1import express from 'express'
2import { generateOTPSecret, isOTPValid } from '@server/helpers/otp' 2import { generateOTPSecret, isOTPValid } from '@server/helpers/otp'
3import { Redis } from '@server/lib/redis' 3import { Redis } from '@server/lib/redis'
4import { asyncMiddleware, authenticate, usersCheckCurrentPassword } from '@server/middlewares' 4import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares'
5import { 5import {
6 confirmTwoFactorValidator, 6 confirmTwoFactorValidator,
7 disableTwoFactorValidator, 7 disableTwoFactorValidator,
@@ -13,7 +13,7 @@ const twoFactorRouter = express.Router()
13 13
14twoFactorRouter.post('/:id/two-factor/request', 14twoFactorRouter.post('/:id/two-factor/request',
15 authenticate, 15 authenticate,
16 asyncMiddleware(usersCheckCurrentPassword), 16 asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)),
17 asyncMiddleware(requestOrConfirmTwoFactorValidator), 17 asyncMiddleware(requestOrConfirmTwoFactorValidator),
18 asyncMiddleware(requestTwoFactor) 18 asyncMiddleware(requestTwoFactor)
19) 19)
@@ -27,7 +27,7 @@ twoFactorRouter.post('/:id/two-factor/confirm-request',
27 27
28twoFactorRouter.post('/:id/two-factor/disable', 28twoFactorRouter.post('/:id/two-factor/disable',
29 authenticate, 29 authenticate,
30 asyncMiddleware(usersCheckCurrentPassword), 30 asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)),
31 asyncMiddleware(disableTwoFactorValidator), 31 asyncMiddleware(disableTwoFactorValidator),
32 asyncMiddleware(disableTwoFactor) 32 asyncMiddleware(disableTwoFactor)
33) 33)
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 () {
24// User password checks 24// User password checks
25 25
26function comparePassword (plainPassword: string, hashPassword: string) { 26function comparePassword (plainPassword: string, hashPassword: string) {
27 if (!plainPassword) return Promise.resolve(false)
28
27 return bcryptComparePromise(plainPassword, hashPassword) 29 return bcryptComparePromise(plainPassword, hashPassword)
28} 30}
29 31
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 = [
506 } 506 }
507] 507]
508 508
509const usersCheckCurrentPassword = [ 509const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
510 body('currentPassword').custom(exists), 510 return [
511 body('currentPassword').optional().custom(exists),
511 512
512 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 513 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
513 if (areValidationErrors(req, res)) return 514 if (areValidationErrors(req, res)) return
514 515
515 const user = res.locals.oauth.token.User 516 const user = res.locals.oauth.token.User
516 if (await user.isPasswordMatch(req.body.currentPassword) !== true) { 517 const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
517 return res.fail({ 518 const targetUserId = parseInt(targetUserIdGetter(req) + '')
518 status: HttpStatusCode.FORBIDDEN_403,
519 message: 'currentPassword is invalid.'
520 })
521 }
522 519
523 return next() 520 // Admin/moderator action on another user, skip the password check
524 } 521 if (isAdminOrModerator && targetUserId !== user.id) {
525] 522 return next()
523 }
524
525 if (!req.body.currentPassword) {
526 return res.fail({
527 status: HttpStatusCode.BAD_REQUEST_400,
528 message: 'currentPassword is missing'
529 })
530 }
531
532 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
533 return res.fail({
534 status: HttpStatusCode.FORBIDDEN_403,
535 message: 'currentPassword is invalid.'
536 })
537 }
538
539 return next()
540 }
541 ]
542}
526 543
527const userAutocompleteValidator = [ 544const userAutocompleteValidator = [
528 param('search') 545 param('search')
@@ -591,7 +608,7 @@ export {
591 usersUpdateValidator, 608 usersUpdateValidator,
592 usersUpdateMeValidator, 609 usersUpdateMeValidator,
593 usersVideoRatingValidator, 610 usersVideoRatingValidator,
594 usersCheckCurrentPassword, 611 usersCheckCurrentPasswordFactory,
595 ensureUserRegistrationAllowed, 612 ensureUserRegistrationAllowed,
596 ensureUserRegistrationAllowedForIP, 613 ensureUserRegistrationAllowedForIP,
597 usersGetValidator, 614 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 () {
86 }) 86 })
87 }) 87 })
88 88
89 it('Should succeed to request two factor without a password when targeting a remote user with an admin account', async function () {
90 await server.twoFactor.request({ userId })
91 })
92
93 it('Should fail to request two factor without a password when targeting myself with an admin account', async function () {
94 await server.twoFactor.request({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
95 await server.twoFactor.request({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
96 })
97
89 it('Should succeed to request my two factor auth', async function () { 98 it('Should succeed to request my two factor auth', async function () {
90 { 99 {
91 const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword }) 100 const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword })
@@ -234,7 +243,7 @@ describe('Test two factor API validators', function () {
234 }) 243 })
235 }) 244 })
236 245
237 it('Should fail to disabled two factor with an incorrect password', async function () { 246 it('Should fail to disable two factor with an incorrect password', async function () {
238 await server.twoFactor.disable({ 247 await server.twoFactor.disable({
239 userId, 248 userId,
240 token: userToken, 249 token: userToken,
@@ -243,16 +252,20 @@ describe('Test two factor API validators', function () {
243 }) 252 })
244 }) 253 })
245 254
255 it('Should succeed to disable two factor without a password when targeting a remote user with an admin account', async function () {
256 await server.twoFactor.disable({ userId })
257 await server.twoFactor.requestAndConfirm({ userId })
258 })
259
260 it('Should fail to disable two factor without a password when targeting myself with an admin account', async function () {
261 await server.twoFactor.disable({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
262 await server.twoFactor.disable({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
263 })
264
246 it('Should succeed to disable another user two factor with the appropriate rights', async function () { 265 it('Should succeed to disable another user two factor with the appropriate rights', async function () {
247 await server.twoFactor.disable({ userId, currentPassword: rootPassword }) 266 await server.twoFactor.disable({ userId, currentPassword: rootPassword })
248 267
249 // Reinit 268 await server.twoFactor.requestAndConfirm({ userId })
250 const { otpRequest } = await server.twoFactor.request({ userId, currentPassword: rootPassword })
251 await server.twoFactor.confirmRequest({
252 userId,
253 requestToken: otpRequest.requestToken,
254 otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate()
255 })
256 }) 269 })
257 270
258 it('Should succeed to update my two factor auth', async function () { 271 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
7 7
8async function login (options: { 8async function login (options: {
9 server: PeerTubeServer 9 server: PeerTubeServer
10 password?: string 10 username: string
11 password: string
11 otpToken?: string 12 otpToken?: string
12 expectedStatus?: HttpStatusCode 13 expectedStatus?: HttpStatusCode
13}) { 14}) {
14 const { server, password = server.store.user.password, otpToken, expectedStatus } = options 15 const { server, username, password, otpToken, expectedStatus } = options
15 16
16 const user = { username: server.store.user.username, password } 17 const user = { username, password }
17 const { res, body: { access_token: token } } = await server.login.loginAndGetResponse({ user, otpToken, expectedStatus }) 18 const { res, body: { access_token: token } } = await server.login.loginAndGetResponse({ user, otpToken, expectedStatus })
18 19
19 return { res, token } 20 return { res, token }
@@ -21,23 +22,28 @@ async function login (options: {
21 22
22describe('Test users', function () { 23describe('Test users', function () {
23 let server: PeerTubeServer 24 let server: PeerTubeServer
24 let rootId: number
25 let otpSecret: string 25 let otpSecret: string
26 let requestToken: string 26 let requestToken: string
27 27
28 const userUsername = 'user1'
29 let userId: number
30 let userPassword: string
31 let userToken: string
32
28 before(async function () { 33 before(async function () {
29 this.timeout(30000) 34 this.timeout(30000)
30 35
31 server = await createSingleServer(1) 36 server = await createSingleServer(1)
32 37
33 await setAccessTokensToServers([ server ]) 38 await setAccessTokensToServers([ server ])
34 39 const res = await server.users.generate(userUsername)
35 const { id } = await server.users.getMyInfo() 40 userId = res.userId
36 rootId = id 41 userPassword = res.password
42 userToken = res.token
37 }) 43 })
38 44
39 it('Should not add the header on login if two factor is not enabled', async function () { 45 it('Should not add the header on login if two factor is not enabled', async function () {
40 const { res, token } = await login({ server }) 46 const { res, token } = await login({ server, username: userUsername, password: userPassword })
41 47
42 expect(res.header['x-peertube-otp']).to.not.exist 48 expect(res.header['x-peertube-otp']).to.not.exist
43 49
@@ -45,10 +51,7 @@ describe('Test users', function () {
45 }) 51 })
46 52
47 it('Should request two factor and get the secret and uri', async function () { 53 it('Should request two factor and get the secret and uri', async function () {
48 const { otpRequest } = await server.twoFactor.request({ 54 const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword })
49 userId: rootId,
50 currentPassword: server.store.user.password
51 })
52 55
53 expect(otpRequest.requestToken).to.exist 56 expect(otpRequest.requestToken).to.exist
54 57
@@ -64,27 +67,33 @@ describe('Test users', function () {
64 }) 67 })
65 68
66 it('Should not have two factor confirmed yet', async function () { 69 it('Should not have two factor confirmed yet', async function () {
67 const { twoFactorEnabled } = await server.users.getMyInfo() 70 const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
68 expect(twoFactorEnabled).to.be.false 71 expect(twoFactorEnabled).to.be.false
69 }) 72 })
70 73
71 it('Should confirm two factor', async function () { 74 it('Should confirm two factor', async function () {
72 await server.twoFactor.confirmRequest({ 75 await server.twoFactor.confirmRequest({
73 userId: rootId, 76 userId,
77 token: userToken,
74 otpToken: TwoFactorCommand.buildOTP({ secret: otpSecret }).generate(), 78 otpToken: TwoFactorCommand.buildOTP({ secret: otpSecret }).generate(),
75 requestToken 79 requestToken
76 }) 80 })
77 }) 81 })
78 82
79 it('Should not add the header on login if two factor is enabled and password is incorrect', async function () { 83 it('Should not add the header on login if two factor is enabled and password is incorrect', async function () {
80 const { res, token } = await login({ server, password: 'fake', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) 84 const { res, token } = await login({ server, username: userUsername, password: 'fake', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
81 85
82 expect(res.header['x-peertube-otp']).to.not.exist 86 expect(res.header['x-peertube-otp']).to.not.exist
83 expect(token).to.not.exist 87 expect(token).to.not.exist
84 }) 88 })
85 89
86 it('Should add the header on login if two factor is enabled and password is correct', async function () { 90 it('Should add the header on login if two factor is enabled and password is correct', async function () {
87 const { res, token } = await login({ server, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) 91 const { res, token } = await login({
92 server,
93 username: userUsername,
94 password: userPassword,
95 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
96 })
88 97
89 expect(res.header['x-peertube-otp']).to.exist 98 expect(res.header['x-peertube-otp']).to.exist
90 expect(token).to.not.exist 99 expect(token).to.not.exist
@@ -95,14 +104,26 @@ describe('Test users', function () {
95 it('Should not login with correct password and incorrect otp secret', async function () { 104 it('Should not login with correct password and incorrect otp secret', async function () {
96 const otp = TwoFactorCommand.buildOTP({ secret: 'a'.repeat(32) }) 105 const otp = TwoFactorCommand.buildOTP({ secret: 'a'.repeat(32) })
97 106
98 const { res, token } = await login({ server, otpToken: otp.generate(), expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) 107 const { res, token } = await login({
108 server,
109 username: userUsername,
110 password: userPassword,
111 otpToken: otp.generate(),
112 expectedStatus: HttpStatusCode.BAD_REQUEST_400
113 })
99 114
100 expect(res.header['x-peertube-otp']).to.not.exist 115 expect(res.header['x-peertube-otp']).to.not.exist
101 expect(token).to.not.exist 116 expect(token).to.not.exist
102 }) 117 })
103 118
104 it('Should not login with correct password and incorrect otp code', async function () { 119 it('Should not login with correct password and incorrect otp code', async function () {
105 const { res, token } = await login({ server, otpToken: '123456', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) 120 const { res, token } = await login({
121 server,
122 username: userUsername,
123 password: userPassword,
124 otpToken: '123456',
125 expectedStatus: HttpStatusCode.BAD_REQUEST_400
126 })
106 127
107 expect(res.header['x-peertube-otp']).to.not.exist 128 expect(res.header['x-peertube-otp']).to.not.exist
108 expect(token).to.not.exist 129 expect(token).to.not.exist
@@ -111,7 +132,13 @@ describe('Test users', function () {
111 it('Should not login with incorrect password and correct otp code', async function () { 132 it('Should not login with incorrect password and correct otp code', async function () {
112 const otpToken = TwoFactorCommand.buildOTP({ secret: otpSecret }).generate() 133 const otpToken = TwoFactorCommand.buildOTP({ secret: otpSecret }).generate()
113 134
114 const { res, token } = await login({ server, password: 'fake', otpToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) 135 const { res, token } = await login({
136 server,
137 username: userUsername,
138 password: 'fake',
139 otpToken,
140 expectedStatus: HttpStatusCode.BAD_REQUEST_400
141 })
115 142
116 expect(res.header['x-peertube-otp']).to.not.exist 143 expect(res.header['x-peertube-otp']).to.not.exist
117 expect(token).to.not.exist 144 expect(token).to.not.exist
@@ -120,7 +147,7 @@ describe('Test users', function () {
120 it('Should correctly login with correct password and otp code', async function () { 147 it('Should correctly login with correct password and otp code', async function () {
121 const otpToken = TwoFactorCommand.buildOTP({ secret: otpSecret }).generate() 148 const otpToken = TwoFactorCommand.buildOTP({ secret: otpSecret }).generate()
122 149
123 const { res, token } = await login({ server, otpToken }) 150 const { res, token } = await login({ server, username: userUsername, password: userPassword, otpToken })
124 151
125 expect(res.header['x-peertube-otp']).to.not.exist 152 expect(res.header['x-peertube-otp']).to.not.exist
126 expect(token).to.exist 153 expect(token).to.exist
@@ -129,21 +156,41 @@ describe('Test users', function () {
129 }) 156 })
130 157
131 it('Should have two factor enabled when getting my info', async function () { 158 it('Should have two factor enabled when getting my info', async function () {
132 const { twoFactorEnabled } = await server.users.getMyInfo() 159 const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
133 expect(twoFactorEnabled).to.be.true 160 expect(twoFactorEnabled).to.be.true
134 }) 161 })
135 162
136 it('Should disable two factor and be able to login without otp token', async function () { 163 it('Should disable two factor and be able to login without otp token', async function () {
137 await server.twoFactor.disable({ userId: rootId, currentPassword: server.store.user.password }) 164 await server.twoFactor.disable({ userId, token: userToken, currentPassword: userPassword })
138 165
139 const { res, token } = await login({ server }) 166 const { res, token } = await login({ server, username: userUsername, password: userPassword })
140 expect(res.header['x-peertube-otp']).to.not.exist 167 expect(res.header['x-peertube-otp']).to.not.exist
141 168
142 await server.users.getMyInfo({ token }) 169 await server.users.getMyInfo({ token })
143 }) 170 })
144 171
145 it('Should have two factor disabled when getting my info', async function () { 172 it('Should have two factor disabled when getting my info', async function () {
146 const { twoFactorEnabled } = await server.users.getMyInfo() 173 const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
174 expect(twoFactorEnabled).to.be.false
175 })
176
177 it('Should enable two factor auth without password from an admin', async function () {
178 const { otpRequest } = await server.twoFactor.request({ userId })
179
180 await server.twoFactor.confirmRequest({
181 userId,
182 otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate(),
183 requestToken: otpRequest.requestToken
184 })
185
186 const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
187 expect(twoFactorEnabled).to.be.true
188 })
189
190 it('Should disable two factor auth without password from an admin', async function () {
191 await server.twoFactor.disable({ userId })
192
193 const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
147 expect(twoFactorEnabled).to.be.false 194 expect(twoFactorEnabled).to.be.false
148 }) 195 })
149 196
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 {
21 21
22 request (options: OverrideCommandOptions & { 22 request (options: OverrideCommandOptions & {
23 userId: number 23 userId: number
24 currentPassword: string 24 currentPassword?: string
25 }) { 25 }) {
26 const { currentPassword, userId } = options 26 const { currentPassword, userId } = options
27 27
@@ -58,7 +58,7 @@ export class TwoFactorCommand extends AbstractCommand {
58 58
59 disable (options: OverrideCommandOptions & { 59 disable (options: OverrideCommandOptions & {
60 userId: number 60 userId: number
61 currentPassword: string 61 currentPassword?: string
62 }) { 62 }) {
63 const { userId, currentPassword } = options 63 const { userId, currentPassword } = options
64 const path = '/api/v1/users/' + userId + '/two-factor/disable' 64 const path = '/api/v1/users/' + userId + '/two-factor/disable'
@@ -72,4 +72,21 @@ export class TwoFactorCommand extends AbstractCommand {
72 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 72 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
73 }) 73 })
74 } 74 }
75
76 async requestAndConfirm (options: OverrideCommandOptions & {
77 userId: number
78 currentPassword?: string
79 }) {
80 const { userId, currentPassword } = options
81
82 const { otpRequest } = await this.request({ userId, currentPassword })
83
84 await this.confirmRequest({
85 userId,
86 requestToken: otpRequest.requestToken,
87 otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate()
88 })
89
90 return otpRequest
91 }
75} 92}