Allow admins to disable two factor auth

This commit is contained in:
Chocobozzz 2022-10-07 14:23:42 +02:00
parent d12b40fb96
commit 2166c058f3
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
17 changed files with 201 additions and 81 deletions

View File

@ -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>

View File

@ -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;
}
} }
} }

View File

@ -60,10 +60,22 @@ export abstract class UserEdit extends FormReactive implements OnInit {
] ]
} }
displayDangerZone () {
if (this.isCreation()) return false
if (this.user?.pluginAuth) return false
if (this.auth.getUser().id === this.user.id) return false
return true
}
resetPassword () { resetPassword () {
return return
} }
disableTwoFactorAuth () {
return
}
getUserVideoQuota () { getUserVideoQuota () {
return this.form.value['videoQuota'] return this.form.value['videoQuota']
} }

View File

@ -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)

View File

@ -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'

View File

@ -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',

View File

@ -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',

View File

@ -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 {
} }

View File

@ -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'

View File

@ -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 { }

View File

@ -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

View File

@ -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)
) )

View File

@ -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)
} }

View File

@ -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,

View File

@ -86,6 +86,15 @@ describe('Test two factor API validators', function () {
}) })
}) })
it('Should succeed to request two factor without a password when targeting a remote user with an admin account', async function () {
await server.twoFactor.request({ userId })
})
it('Should fail to request two factor without a password when targeting myself with an admin account', async function () {
await server.twoFactor.request({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
await server.twoFactor.request({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
})
it('Should succeed to request my two factor auth', async function () { 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 () {

View File

@ -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
}) })

View File

@ -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
}
} }