diff options
author | Chocobozzz <me@florianbigard.com> | 2022-10-07 14:23:42 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-10-07 14:28:35 +0200 |
commit | 2166c058f34dff6f91566930d12448805d829de7 (patch) | |
tree | 2b9100b8eccbac287d1105c765901f966a354986 | |
parent | d12b40fb96d56786a96c06a621f3d8e0a0d24f4a (diff) | |
download | PeerTube-2166c058f34dff6f91566930d12448805d829de7.tar.gz PeerTube-2166c058f34dff6f91566930d12448805d829de7.tar.zst PeerTube-2166c058f34dff6f91566930d12448805d829de7.zip |
Allow admins to disable two factor auth
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' |
12 | import { FormValidatorService } from '@app/shared/shared-forms' | 12 | import { FormValidatorService } from '@app/shared/shared-forms' |
13 | import { UserAdminService } from '@app/shared/shared-users' | 13 | import { TwoFactorService, UserAdminService } from '@app/shared/shared-users' |
14 | import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models' | 14 | import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models' |
15 | import { UserEdit } from './user-edit' | 15 | import { 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 @@ | |||
1 | export * from './my-account-two-factor-button.component' | 1 | export * from './my-account-two-factor-button.component' |
2 | export * from './my-account-two-factor.component' | 2 | export * from './my-account-two-factor.component' |
3 | 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 @@ | |||
1 | import { Subject } from 'rxjs' | 1 | import { Subject } from 'rxjs' |
2 | import { Component, Input, OnInit } from '@angular/core' | 2 | import { Component, Input, OnInit } from '@angular/core' |
3 | import { AuthService, ConfirmService, Notifier, User } from '@app/core' | 3 | import { AuthService, ConfirmService, Notifier, User } from '@app/core' |
4 | import { TwoFactorService } from './two-factor.service' | 4 | import { 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' | |||
4 | import { AuthService, Notifier, User } from '@app/core' | 4 | import { AuthService, Notifier, User } from '@app/core' |
5 | import { USER_EXISTING_PASSWORD_VALIDATOR, USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-validators' | 5 | import { USER_EXISTING_PASSWORD_VALIDATOR, USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-validators' |
6 | import { FormReactiveService } from '@app/shared/shared-forms' | 6 | import { FormReactiveService } from '@app/shared/shared-forms' |
7 | import { TwoFactorService } from './two-factor.service' | 7 | import { 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' | |||
11 | import { SharedModerationModule } from '@app/shared/shared-moderation' | 11 | import { SharedModerationModule } from '@app/shared/shared-moderation' |
12 | import { SharedShareModal } from '@app/shared/shared-share-modal' | 12 | import { SharedShareModal } from '@app/shared/shared-share-modal' |
13 | import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' | 13 | import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' |
14 | import { SharedUsersModule } from '@app/shared/shared-users' | ||
14 | import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module' | 15 | import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module' |
15 | import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' | 16 | import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' |
16 | import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' | 17 | import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' |
@@ -24,11 +25,7 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d | |||
24 | import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' | 25 | import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' |
25 | import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' | 26 | import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' |
26 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' | 27 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' |
27 | import { | 28 | import { 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' | ||
32 | import { MyAccountComponent } from './my-account.component' | 29 | import { 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 | }) |
81 | export class MyAccountModule { | 77 | export 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 @@ | |||
1 | export * from './user-admin.service' | 1 | export * from './user-admin.service' |
2 | export * from './user-signup.service' | 2 | export * from './user-signup.service' |
3 | export * from './two-factor.service' | ||
3 | 4 | ||
4 | export * from './shared-users.module' | 5 | 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 @@ | |||
1 | 1 | ||
2 | import { NgModule } from '@angular/core' | 2 | import { NgModule } from '@angular/core' |
3 | import { SharedMainModule } from '../shared-main/shared-main.module' | 3 | import { SharedMainModule } from '../shared-main/shared-main.module' |
4 | import { TwoFactorService } from './two-factor.service' | ||
4 | import { UserAdminService } from './user-admin.service' | 5 | import { UserAdminService } from './user-admin.service' |
5 | import { UserSignupService } from './user-signup.service' | 6 | import { 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 | }) |
21 | export class SharedUsersModule { } | 23 | export 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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { generateOTPSecret, isOTPValid } from '@server/helpers/otp' | 2 | import { generateOTPSecret, isOTPValid } from '@server/helpers/otp' |
3 | import { Redis } from '@server/lib/redis' | 3 | import { Redis } from '@server/lib/redis' |
4 | import { asyncMiddleware, authenticate, usersCheckCurrentPassword } from '@server/middlewares' | 4 | import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares' |
5 | import { | 5 | import { |
6 | confirmTwoFactorValidator, | 6 | confirmTwoFactorValidator, |
7 | disableTwoFactorValidator, | 7 | disableTwoFactorValidator, |
@@ -13,7 +13,7 @@ const twoFactorRouter = express.Router() | |||
13 | 13 | ||
14 | twoFactorRouter.post('/:id/two-factor/request', | 14 | twoFactorRouter.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 | ||
28 | twoFactorRouter.post('/:id/two-factor/disable', | 28 | twoFactorRouter.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 | ||
26 | function comparePassword (plainPassword: string, hashPassword: string) { | 26 | function 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 | ||
509 | const usersCheckCurrentPassword = [ | 509 | const 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 | ||
527 | const userAutocompleteValidator = [ | 544 | const 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 | ||
8 | async function login (options: { | 8 | async 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 | ||
22 | describe('Test users', function () { | 23 | describe('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 | } |