Allow admins to disable two factor auth
This commit is contained in:
parent
d12b40fb96
commit
2166c058f3
|
@ -204,7 +204,7 @@
|
||||||
</div>
|
</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="col-12 col-lg-4 col-xl-3">
|
||||||
<div class="anchor" id="danger"></div> <!-- danger zone anchor -->
|
<div class="anchor" id="danger"></div> <!-- danger zone anchor -->
|
||||||
<div i18n class="account-title account-title-danger">DANGER ZONE</div>
|
<div i18n class="account-title account-title-danger">DANGER ZONE</div>
|
||||||
|
@ -213,7 +213,7 @@
|
||||||
<div class="col-12 col-lg-8 col-xl-9">
|
<div class="col-12 col-lg-8 col-xl-9">
|
||||||
|
|
||||||
<div class="danger-zone">
|
<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>
|
<label i18n>Send a link to reset the password by email to the user</label>
|
||||||
<button (click)="resetPassword()" i18n>Ask for new password</button>
|
<button (click)="resetPassword()" i18n>Ask for new password</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -222,6 +222,11 @@
|
||||||
<label i18n>Manually set the user password</label>
|
<label i18n>Manually set the user password</label>
|
||||||
<my-user-password [userId]="user.id"></my-user-password>
|
<my-user-password [userId]="user.id"></my-user-password>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -48,17 +48,13 @@ my-user-real-quota-info {
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger-zone {
|
.danger-zone {
|
||||||
.reset-password-email {
|
button {
|
||||||
margin-bottom: 30px;
|
@include peertube-button;
|
||||||
|
@include danger-button;
|
||||||
|
@include disable-outline;
|
||||||
|
|
||||||
button {
|
display: block;
|
||||||
@include peertube-button;
|
margin-top: 0;
|
||||||
@include danger-button;
|
|
||||||
@include disable-outline;
|
|
||||||
|
|
||||||
display: block;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 () {
|
resetPassword () {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disableTwoFactorAuth () {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
getUserVideoQuota () {
|
getUserVideoQuota () {
|
||||||
return this.form.value['videoQuota']
|
return this.form.value['videoQuota']
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
USER_VIDEO_QUOTA_VALIDATOR
|
USER_VIDEO_QUOTA_VALIDATOR
|
||||||
} from '@app/shared/form-validators/user-validators'
|
} from '@app/shared/form-validators/user-validators'
|
||||||
import { FormValidatorService } from '@app/shared/shared-forms'
|
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 { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models'
|
||||||
import { UserEdit } from './user-edit'
|
import { UserEdit } from './user-edit'
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
|
private twoFactorService: TwoFactorService,
|
||||||
private userAdminService: UserAdminService
|
private userAdminService: UserAdminService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
@ -120,12 +121,24 @@ 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}.`)
|
this.notifier.success($localize`An email asking for password reset has been sent to ${this.user.username}.`)
|
||||||
},
|
},
|
||||||
|
|
||||||
error: err => {
|
error: err => this.notifier.error(err.message)
|
||||||
this.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) {
|
private onUserFetched (userJson: UserType) {
|
||||||
this.user = new User(userJson)
|
this.user = new User(userJson)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
export * from './my-account-two-factor-button.component'
|
export * from './my-account-two-factor-button.component'
|
||||||
export * from './my-account-two-factor.component'
|
export * from './my-account-two-factor.component'
|
||||||
export * from './two-factor.service'
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
import { Component, Input, OnInit } from '@angular/core'
|
import { Component, Input, OnInit } from '@angular/core'
|
||||||
import { AuthService, ConfirmService, Notifier, User } from '@app/core'
|
import { AuthService, ConfirmService, Notifier, User } from '@app/core'
|
||||||
import { TwoFactorService } from './two-factor.service'
|
import { TwoFactorService } from '@app/shared/shared-users'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-two-factor-button',
|
selector: 'my-account-two-factor-button',
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Router } from '@angular/router'
|
||||||
import { AuthService, Notifier, User } from '@app/core'
|
import { AuthService, Notifier, User } from '@app/core'
|
||||||
import { USER_EXISTING_PASSWORD_VALIDATOR, USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
import { USER_EXISTING_PASSWORD_VALIDATOR, USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
||||||
import { FormReactiveService } from '@app/shared/shared-forms'
|
import { FormReactiveService } from '@app/shared/shared-forms'
|
||||||
import { TwoFactorService } from './two-factor.service'
|
import { TwoFactorService } from '@app/shared/shared-users'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-two-factor',
|
selector: 'my-account-two-factor',
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { SharedMainModule } from '@app/shared/shared-main'
|
||||||
import { SharedModerationModule } from '@app/shared/shared-moderation'
|
import { SharedModerationModule } from '@app/shared/shared-moderation'
|
||||||
import { SharedShareModal } from '@app/shared/shared-share-modal'
|
import { SharedShareModal } from '@app/shared/shared-share-modal'
|
||||||
import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings'
|
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 { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
|
||||||
import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
|
import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
|
||||||
import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.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 { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences'
|
||||||
import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component'
|
import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component'
|
||||||
import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
|
import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
|
||||||
import {
|
import { MyAccountTwoFactorButtonComponent, MyAccountTwoFactorComponent } from './my-account-settings/my-account-two-factor'
|
||||||
MyAccountTwoFactorButtonComponent,
|
|
||||||
MyAccountTwoFactorComponent,
|
|
||||||
TwoFactorService
|
|
||||||
} from './my-account-settings/my-account-two-factor'
|
|
||||||
import { MyAccountComponent } from './my-account.component'
|
import { MyAccountComponent } from './my-account.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -44,6 +41,7 @@ import { MyAccountComponent } from './my-account.component'
|
||||||
SharedFormModule,
|
SharedFormModule,
|
||||||
SharedModerationModule,
|
SharedModerationModule,
|
||||||
SharedUserInterfaceSettingsModule,
|
SharedUserInterfaceSettingsModule,
|
||||||
|
SharedUsersModule,
|
||||||
SharedGlobalIconModule,
|
SharedGlobalIconModule,
|
||||||
SharedAbuseListModule,
|
SharedAbuseListModule,
|
||||||
SharedShareModal,
|
SharedShareModal,
|
||||||
|
@ -74,9 +72,7 @@ import { MyAccountComponent } from './my-account.component'
|
||||||
MyAccountComponent
|
MyAccountComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
providers: [
|
providers: []
|
||||||
TwoFactorService
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class MyAccountModule {
|
export class MyAccountModule {
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './user-admin.service'
|
export * from './user-admin.service'
|
||||||
export * from './user-signup.service'
|
export * from './user-signup.service'
|
||||||
|
export * from './two-factor.service'
|
||||||
|
|
||||||
export * from './shared-users.module'
|
export * from './shared-users.module'
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { SharedMainModule } from '../shared-main/shared-main.module'
|
import { SharedMainModule } from '../shared-main/shared-main.module'
|
||||||
|
import { TwoFactorService } from './two-factor.service'
|
||||||
import { UserAdminService } from './user-admin.service'
|
import { UserAdminService } from './user-admin.service'
|
||||||
import { UserSignupService } from './user-signup.service'
|
import { UserSignupService } from './user-signup.service'
|
||||||
|
|
||||||
|
@ -15,7 +16,8 @@ import { UserSignupService } from './user-signup.service'
|
||||||
|
|
||||||
providers: [
|
providers: [
|
||||||
UserSignupService,
|
UserSignupService,
|
||||||
UserAdminService
|
UserAdminService,
|
||||||
|
TwoFactorService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SharedUsersModule { }
|
export class SharedUsersModule { }
|
||||||
|
|
|
@ -40,7 +40,7 @@ export class TwoFactorService {
|
||||||
|
|
||||||
disableTwoFactor (options: {
|
disableTwoFactor (options: {
|
||||||
userId: number
|
userId: number
|
||||||
currentPassword: string
|
currentPassword?: string
|
||||||
}) {
|
}) {
|
||||||
const { userId, currentPassword } = options
|
const { userId, currentPassword } = options
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { generateOTPSecret, isOTPValid } from '@server/helpers/otp'
|
import { generateOTPSecret, isOTPValid } from '@server/helpers/otp'
|
||||||
import { Redis } from '@server/lib/redis'
|
import { Redis } from '@server/lib/redis'
|
||||||
import { asyncMiddleware, authenticate, usersCheckCurrentPassword } from '@server/middlewares'
|
import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares'
|
||||||
import {
|
import {
|
||||||
confirmTwoFactorValidator,
|
confirmTwoFactorValidator,
|
||||||
disableTwoFactorValidator,
|
disableTwoFactorValidator,
|
||||||
|
@ -13,7 +13,7 @@ const twoFactorRouter = express.Router()
|
||||||
|
|
||||||
twoFactorRouter.post('/:id/two-factor/request',
|
twoFactorRouter.post('/:id/two-factor/request',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(usersCheckCurrentPassword),
|
asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)),
|
||||||
asyncMiddleware(requestOrConfirmTwoFactorValidator),
|
asyncMiddleware(requestOrConfirmTwoFactorValidator),
|
||||||
asyncMiddleware(requestTwoFactor)
|
asyncMiddleware(requestTwoFactor)
|
||||||
)
|
)
|
||||||
|
@ -27,7 +27,7 @@ twoFactorRouter.post('/:id/two-factor/confirm-request',
|
||||||
|
|
||||||
twoFactorRouter.post('/:id/two-factor/disable',
|
twoFactorRouter.post('/:id/two-factor/disable',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(usersCheckCurrentPassword),
|
asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)),
|
||||||
asyncMiddleware(disableTwoFactorValidator),
|
asyncMiddleware(disableTwoFactorValidator),
|
||||||
asyncMiddleware(disableTwoFactor)
|
asyncMiddleware(disableTwoFactor)
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,6 +24,8 @@ function createPrivateAndPublicKeys () {
|
||||||
// User password checks
|
// User password checks
|
||||||
|
|
||||||
function comparePassword (plainPassword: string, hashPassword: string) {
|
function comparePassword (plainPassword: string, hashPassword: string) {
|
||||||
|
if (!plainPassword) return Promise.resolve(false)
|
||||||
|
|
||||||
return bcryptComparePromise(plainPassword, hashPassword)
|
return bcryptComparePromise(plainPassword, hashPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -506,23 +506,40 @@ const usersVerifyEmailValidator = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const usersCheckCurrentPassword = [
|
const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
|
||||||
body('currentPassword').custom(exists),
|
return [
|
||||||
|
body('currentPassword').optional().custom(exists),
|
||||||
|
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
const user = res.locals.oauth.token.User
|
const user = res.locals.oauth.token.User
|
||||||
if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
|
const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
|
||||||
return res.fail({
|
const targetUserId = parseInt(targetUserIdGetter(req) + '')
|
||||||
status: HttpStatusCode.FORBIDDEN_403,
|
|
||||||
message: 'currentPassword is invalid.'
|
// 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()
|
||||||
}
|
}
|
||||||
|
]
|
||||||
return next()
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const userAutocompleteValidator = [
|
const userAutocompleteValidator = [
|
||||||
param('search')
|
param('search')
|
||||||
|
@ -591,7 +608,7 @@ export {
|
||||||
usersUpdateValidator,
|
usersUpdateValidator,
|
||||||
usersUpdateMeValidator,
|
usersUpdateMeValidator,
|
||||||
usersVideoRatingValidator,
|
usersVideoRatingValidator,
|
||||||
usersCheckCurrentPassword,
|
usersCheckCurrentPasswordFactory,
|
||||||
ensureUserRegistrationAllowed,
|
ensureUserRegistrationAllowed,
|
||||||
ensureUserRegistrationAllowedForIP,
|
ensureUserRegistrationAllowedForIP,
|
||||||
usersGetValidator,
|
usersGetValidator,
|
||||||
|
|
|
@ -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 () {
|
it('Should succeed to request my two factor auth', async function () {
|
||||||
{
|
{
|
||||||
const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword })
|
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({
|
await server.twoFactor.disable({
|
||||||
userId,
|
userId,
|
||||||
token: userToken,
|
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 () {
|
it('Should succeed to disable another user two factor with the appropriate rights', async function () {
|
||||||
await server.twoFactor.disable({ userId, currentPassword: rootPassword })
|
await server.twoFactor.disable({ userId, currentPassword: rootPassword })
|
||||||
|
|
||||||
// Reinit
|
await server.twoFactor.requestAndConfirm({ userId })
|
||||||
const { otpRequest } = await server.twoFactor.request({ userId, currentPassword: rootPassword })
|
|
||||||
await server.twoFactor.confirmRequest({
|
|
||||||
userId,
|
|
||||||
requestToken: otpRequest.requestToken,
|
|
||||||
otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should succeed to update my two factor auth', async function () {
|
it('Should succeed to update my two factor auth', async function () {
|
||||||
|
|
|
@ -7,13 +7,14 @@ import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServ
|
||||||
|
|
||||||
async function login (options: {
|
async function login (options: {
|
||||||
server: PeerTubeServer
|
server: PeerTubeServer
|
||||||
password?: string
|
username: string
|
||||||
|
password: string
|
||||||
otpToken?: string
|
otpToken?: string
|
||||||
expectedStatus?: HttpStatusCode
|
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 })
|
const { res, body: { access_token: token } } = await server.login.loginAndGetResponse({ user, otpToken, expectedStatus })
|
||||||
|
|
||||||
return { res, token }
|
return { res, token }
|
||||||
|
@ -21,23 +22,28 @@ async function login (options: {
|
||||||
|
|
||||||
describe('Test users', function () {
|
describe('Test users', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
let rootId: number
|
|
||||||
let otpSecret: string
|
let otpSecret: string
|
||||||
let requestToken: string
|
let requestToken: string
|
||||||
|
|
||||||
|
const userUsername = 'user1'
|
||||||
|
let userId: number
|
||||||
|
let userPassword: string
|
||||||
|
let userToken: string
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(30000)
|
this.timeout(30000)
|
||||||
|
|
||||||
server = await createSingleServer(1)
|
server = await createSingleServer(1)
|
||||||
|
|
||||||
await setAccessTokensToServers([ server ])
|
await setAccessTokensToServers([ server ])
|
||||||
|
const res = await server.users.generate(userUsername)
|
||||||
const { id } = await server.users.getMyInfo()
|
userId = res.userId
|
||||||
rootId = id
|
userPassword = res.password
|
||||||
|
userToken = res.token
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not add the header on login if two factor is not enabled', async function () {
|
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
|
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 () {
|
it('Should request two factor and get the secret and uri', async function () {
|
||||||
const { otpRequest } = await server.twoFactor.request({
|
const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword })
|
||||||
userId: rootId,
|
|
||||||
currentPassword: server.store.user.password
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(otpRequest.requestToken).to.exist
|
expect(otpRequest.requestToken).to.exist
|
||||||
|
|
||||||
|
@ -64,27 +67,33 @@ describe('Test users', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not have two factor confirmed yet', async 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
|
expect(twoFactorEnabled).to.be.false
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should confirm two factor', async function () {
|
it('Should confirm two factor', async function () {
|
||||||
await server.twoFactor.confirmRequest({
|
await server.twoFactor.confirmRequest({
|
||||||
userId: rootId,
|
userId,
|
||||||
|
token: userToken,
|
||||||
otpToken: TwoFactorCommand.buildOTP({ secret: otpSecret }).generate(),
|
otpToken: TwoFactorCommand.buildOTP({ secret: otpSecret }).generate(),
|
||||||
requestToken
|
requestToken
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not add the header on login if two factor is enabled and password is incorrect', async function () {
|
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(res.header['x-peertube-otp']).to.not.exist
|
||||||
expect(token).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 () {
|
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(res.header['x-peertube-otp']).to.exist
|
||||||
expect(token).to.not.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 () {
|
it('Should not login with correct password and incorrect otp secret', async function () {
|
||||||
const otp = TwoFactorCommand.buildOTP({ secret: 'a'.repeat(32) })
|
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(res.header['x-peertube-otp']).to.not.exist
|
||||||
expect(token).to.not.exist
|
expect(token).to.not.exist
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not login with correct password and incorrect otp code', async function () {
|
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(res.header['x-peertube-otp']).to.not.exist
|
||||||
expect(token).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 () {
|
it('Should not login with incorrect password and correct otp code', async function () {
|
||||||
const otpToken = TwoFactorCommand.buildOTP({ secret: otpSecret }).generate()
|
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(res.header['x-peertube-otp']).to.not.exist
|
||||||
expect(token).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 () {
|
it('Should correctly login with correct password and otp code', async function () {
|
||||||
const otpToken = TwoFactorCommand.buildOTP({ secret: otpSecret }).generate()
|
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(res.header['x-peertube-otp']).to.not.exist
|
||||||
expect(token).to.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 () {
|
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
|
expect(twoFactorEnabled).to.be.true
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should disable two factor and be able to login without otp token', async function () {
|
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
|
expect(res.header['x-peertube-otp']).to.not.exist
|
||||||
|
|
||||||
await server.users.getMyInfo({ token })
|
await server.users.getMyInfo({ token })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should have two factor disabled when getting my info', async function () {
|
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
|
expect(twoFactorEnabled).to.be.false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ export class TwoFactorCommand extends AbstractCommand {
|
||||||
|
|
||||||
request (options: OverrideCommandOptions & {
|
request (options: OverrideCommandOptions & {
|
||||||
userId: number
|
userId: number
|
||||||
currentPassword: string
|
currentPassword?: string
|
||||||
}) {
|
}) {
|
||||||
const { currentPassword, userId } = options
|
const { currentPassword, userId } = options
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ export class TwoFactorCommand extends AbstractCommand {
|
||||||
|
|
||||||
disable (options: OverrideCommandOptions & {
|
disable (options: OverrideCommandOptions & {
|
||||||
userId: number
|
userId: number
|
||||||
currentPassword: string
|
currentPassword?: string
|
||||||
}) {
|
}) {
|
||||||
const { userId, currentPassword } = options
|
const { userId, currentPassword } = options
|
||||||
const path = '/api/v1/users/' + userId + '/two-factor/disable'
|
const path = '/api/v1/users/' + userId + '/two-factor/disable'
|
||||||
|
@ -72,4 +72,21 @@ export class TwoFactorCommand extends AbstractCommand {
|
||||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue