chore: Add serverPassword param to endpoints (#2919) [skip e2e]
* chore: send server password param to delete account endpoint * chore: send server password param to disable mfa endpoint * chore: modify tests * chore: force challenge prompt for mfa disable * chore: fix eslint errors * chore: add server passsword to get recovery codes * chore: fix tests * chore: pass server password as header
This commit is contained in:
committed by
GitHub
parent
cf4d2196de
commit
54af28aa04
@@ -22,7 +22,9 @@ export class AuthApiService implements AuthApiServiceInterface {
|
|||||||
this.operationsInProgress = new Map()
|
this.operationsInProgress = new Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateRecoveryCodes(): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>> {
|
async generateRecoveryCodes(dto: {
|
||||||
|
serverPassword: string
|
||||||
|
}): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>> {
|
||||||
if (this.operationsInProgress.get(AuthApiOperations.GenerateRecoveryCodes)) {
|
if (this.operationsInProgress.get(AuthApiOperations.GenerateRecoveryCodes)) {
|
||||||
throw new ApiCallError(ErrorMessage.GenericInProgress)
|
throw new ApiCallError(ErrorMessage.GenericInProgress)
|
||||||
}
|
}
|
||||||
@@ -30,7 +32,9 @@ export class AuthApiService implements AuthApiServiceInterface {
|
|||||||
this.operationsInProgress.set(AuthApiOperations.GenerateRecoveryCodes, true)
|
this.operationsInProgress.set(AuthApiOperations.GenerateRecoveryCodes, true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.authServer.generateRecoveryCodes()
|
const response = await this.authServer.generateRecoveryCodes({
|
||||||
|
headers: [{ key: 'x-server-password', value: dto.serverPassword }],
|
||||||
|
})
|
||||||
|
|
||||||
return response
|
return response
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from '../../Response'
|
} from '../../Response'
|
||||||
|
|
||||||
export interface AuthApiServiceInterface {
|
export interface AuthApiServiceInterface {
|
||||||
generateRecoveryCodes(): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>>
|
generateRecoveryCodes(dto: { serverPassword: string }): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>>
|
||||||
recoveryKeyParams(dto: {
|
recoveryKeyParams(dto: {
|
||||||
username: string
|
username: string
|
||||||
codeChallenge: string
|
codeChallenge: string
|
||||||
|
|||||||
@@ -27,13 +27,19 @@ export class UserApiService implements UserApiServiceInterface {
|
|||||||
this.operationsInProgress = new Map()
|
this.operationsInProgress = new Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAccount(userUuid: string): Promise<HttpResponse<UserDeletionResponseBody>> {
|
async deleteAccount(dto: {
|
||||||
|
userUuid: string
|
||||||
|
serverPassword: string
|
||||||
|
}): Promise<HttpResponse<UserDeletionResponseBody>> {
|
||||||
this.lockOperation(UserApiOperations.DeletingAccount)
|
this.lockOperation(UserApiOperations.DeletingAccount)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.userServer.deleteAccount({
|
const response = await this.userServer.deleteAccount(
|
||||||
userUuid: userUuid,
|
{
|
||||||
})
|
userUuid: dto.userUuid,
|
||||||
|
},
|
||||||
|
{ headers: [{ key: 'x-server-password', value: dto.serverPassword }] },
|
||||||
|
)
|
||||||
|
|
||||||
this.unlockOperation(UserApiOperations.DeletingAccount)
|
this.unlockOperation(UserApiOperations.DeletingAccount)
|
||||||
|
|
||||||
|
|||||||
@@ -22,5 +22,8 @@ export interface UserApiServiceInterface {
|
|||||||
requestType: UserRequestType
|
requestType: UserRequestType
|
||||||
}): Promise<HttpResponse<UserRequestResponseBody>>
|
}): Promise<HttpResponse<UserRequestResponseBody>>
|
||||||
|
|
||||||
deleteAccount(userUuid: string): Promise<HttpResponse<UserDeletionResponseBody>>
|
deleteAccount(dto: {
|
||||||
|
userUuid: string
|
||||||
|
serverPassword: string | undefined
|
||||||
|
}): Promise<HttpResponse<UserDeletionResponseBody>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export class HttpService implements HttpServiceInterface {
|
|||||||
params,
|
params,
|
||||||
verb: HttpVerb.Get,
|
verb: HttpVerb.Get,
|
||||||
authentication: options?.authentication ?? this.getSessionAccessToken(),
|
authentication: options?.authentication ?? this.getSessionAccessToken(),
|
||||||
|
customHeaders: options?.headers,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +124,7 @@ export class HttpService implements HttpServiceInterface {
|
|||||||
params,
|
params,
|
||||||
verb: HttpVerb.Put,
|
verb: HttpVerb.Put,
|
||||||
authentication: options?.authentication ?? this.getSessionAccessToken(),
|
authentication: options?.authentication ?? this.getSessionAccessToken(),
|
||||||
|
customHeaders: options?.headers,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +143,7 @@ export class HttpService implements HttpServiceInterface {
|
|||||||
params,
|
params,
|
||||||
verb: HttpVerb.Delete,
|
verb: HttpVerb.Delete,
|
||||||
authentication: options?.authentication ?? this.getSessionAccessToken(),
|
authentication: options?.authentication ?? this.getSessionAccessToken(),
|
||||||
|
customHeaders: options?.headers,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface GenerateRecoveryCodesRequestParams {
|
||||||
|
serverPassword: string
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ export * from './Authenticator/DeleteAuthenticatorRequestParams'
|
|||||||
export * from './Authenticator/GenerateAuthenticatorAuthenticationOptionsRequestParams'
|
export * from './Authenticator/GenerateAuthenticatorAuthenticationOptionsRequestParams'
|
||||||
export * from './Authenticator/ListAuthenticatorsRequestParams'
|
export * from './Authenticator/ListAuthenticatorsRequestParams'
|
||||||
export * from './Authenticator/VerifyAuthenticatorRegistrationResponseRequestParams'
|
export * from './Authenticator/VerifyAuthenticatorRegistrationResponseRequestParams'
|
||||||
|
export * from './Recovery/GenerateRecoveryCodesRequestParams'
|
||||||
export * from './Recovery/RecoveryKeyParamsRequestParams'
|
export * from './Recovery/RecoveryKeyParamsRequestParams'
|
||||||
export * from './Recovery/SignInWithRecoveryCodesRequestParams'
|
export * from './Recovery/SignInWithRecoveryCodesRequestParams'
|
||||||
export * from './Revision/DeleteRevisionRequestParams'
|
export * from './Revision/DeleteRevisionRequestParams'
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import {
|
|||||||
} from '../../Response'
|
} from '../../Response'
|
||||||
import { AuthServerInterface } from './AuthServerInterface'
|
import { AuthServerInterface } from './AuthServerInterface'
|
||||||
import { Paths } from './Paths'
|
import { Paths } from './Paths'
|
||||||
|
import { HttpRequestOptions } from '../../Http/HttpRequestOptions'
|
||||||
|
|
||||||
export class AuthServer implements AuthServerInterface {
|
export class AuthServer implements AuthServerInterface {
|
||||||
constructor(private httpService: HttpServiceInterface) {}
|
constructor(private httpService: HttpServiceInterface) {}
|
||||||
|
|
||||||
async generateRecoveryCodes(): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>> {
|
async generateRecoveryCodes(options?: HttpRequestOptions): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>> {
|
||||||
return this.httpService.post(Paths.v1.generateRecoveryCodes)
|
return this.httpService.post(Paths.v1.generateRecoveryCodes, undefined, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
async recoveryKeyParams(
|
async recoveryKeyParams(
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import {
|
|||||||
RecoveryKeyParamsResponseBody,
|
RecoveryKeyParamsResponseBody,
|
||||||
SignInWithRecoveryCodesResponseBody,
|
SignInWithRecoveryCodesResponseBody,
|
||||||
} from '../../Response'
|
} from '../../Response'
|
||||||
|
import { HttpRequestOptions } from '../../Http/HttpRequestOptions'
|
||||||
|
|
||||||
export interface AuthServerInterface {
|
export interface AuthServerInterface {
|
||||||
generateRecoveryCodes(): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>>
|
generateRecoveryCodes(options?: HttpRequestOptions): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>>
|
||||||
recoveryKeyParams(params: RecoveryKeyParamsRequestParams): Promise<HttpResponse<RecoveryKeyParamsResponseBody>>
|
recoveryKeyParams(params: RecoveryKeyParamsRequestParams): Promise<HttpResponse<RecoveryKeyParamsResponseBody>>
|
||||||
signInWithRecoveryCodes(
|
signInWithRecoveryCodes(
|
||||||
params: SignInWithRecoveryCodesRequestParams,
|
params: SignInWithRecoveryCodesRequestParams,
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ import { UserRegistrationResponseBody } from '../../Response/User/UserRegistrati
|
|||||||
import { Paths } from './Paths'
|
import { Paths } from './Paths'
|
||||||
import { UserServerInterface } from './UserServerInterface'
|
import { UserServerInterface } from './UserServerInterface'
|
||||||
import { UserUpdateRequestParams } from '../../Request/User/UserUpdateRequestParams'
|
import { UserUpdateRequestParams } from '../../Request/User/UserUpdateRequestParams'
|
||||||
|
import { HttpRequestOptions } from '../../Http/HttpRequestOptions'
|
||||||
|
|
||||||
export class UserServer implements UserServerInterface {
|
export class UserServer implements UserServerInterface {
|
||||||
constructor(private httpService: HttpServiceInterface) {}
|
constructor(private httpService: HttpServiceInterface) {}
|
||||||
|
|
||||||
async deleteAccount(params: UserDeletionRequestParams): Promise<HttpResponse<UserDeletionResponseBody>> {
|
async deleteAccount(
|
||||||
return this.httpService.delete(Paths.v1.deleteAccount(params.userUuid), params)
|
params: UserDeletionRequestParams,
|
||||||
|
options?: HttpRequestOptions,
|
||||||
|
): Promise<HttpResponse<UserDeletionResponseBody>> {
|
||||||
|
return this.httpService.delete(Paths.v1.deleteAccount(params.userUuid), params, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(params: UserRegistrationRequestParams): Promise<HttpResponse<UserRegistrationResponseBody>> {
|
async register(params: UserRegistrationRequestParams): Promise<HttpResponse<UserRegistrationResponseBody>> {
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ import { UserDeletionResponseBody } from '../../Response/User/UserDeletionRespon
|
|||||||
import { UserRegistrationResponseBody } from '../../Response/User/UserRegistrationResponseBody'
|
import { UserRegistrationResponseBody } from '../../Response/User/UserRegistrationResponseBody'
|
||||||
import { UserUpdateResponse } from '../../Response/User/UserUpdateResponse'
|
import { UserUpdateResponse } from '../../Response/User/UserUpdateResponse'
|
||||||
import { UserUpdateRequestParams } from '../../Request/User/UserUpdateRequestParams'
|
import { UserUpdateRequestParams } from '../../Request/User/UserUpdateRequestParams'
|
||||||
|
import { HttpRequestOptions } from '../../Http/HttpRequestOptions'
|
||||||
|
|
||||||
export interface UserServerInterface {
|
export interface UserServerInterface {
|
||||||
register(params: UserRegistrationRequestParams): Promise<HttpResponse<UserRegistrationResponseBody>>
|
register(params: UserRegistrationRequestParams): Promise<HttpResponse<UserRegistrationResponseBody>>
|
||||||
deleteAccount(params: UserDeletionRequestParams): Promise<HttpResponse<UserDeletionResponseBody>>
|
deleteAccount(
|
||||||
|
params: UserDeletionRequestParams,
|
||||||
|
options?: HttpRequestOptions,
|
||||||
|
): Promise<HttpResponse<UserDeletionResponseBody>>
|
||||||
update(params: UserUpdateRequestParams): Promise<HttpResponse<UserUpdateResponse>>
|
update(params: UserUpdateRequestParams): Promise<HttpResponse<UserUpdateResponse>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { AnyKeyParamsContent } from '@standardnotes/common'
|
|||||||
import { SessionBody } from '@standardnotes/responses'
|
import { SessionBody } from '@standardnotes/responses'
|
||||||
|
|
||||||
export interface AuthClientInterface {
|
export interface AuthClientInterface {
|
||||||
generateRecoveryCodes(): Promise<string | false>
|
generateRecoveryCodes(dto: { serverPassword: string }): Promise<string | false>
|
||||||
recoveryKeyParams(dto: {
|
recoveryKeyParams(dto: {
|
||||||
username: string
|
username: string
|
||||||
codeChallenge: string
|
codeChallenge: string
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ export class AuthManager extends AbstractService implements AuthClientInterface
|
|||||||
super(internalEventBus)
|
super(internalEventBus)
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateRecoveryCodes(): Promise<string | false> {
|
async generateRecoveryCodes(dto: { serverPassword: string }): Promise<string | false> {
|
||||||
try {
|
try {
|
||||||
const result = await this.authApiService.generateRecoveryCodes()
|
const result = await this.authApiService.generateRecoveryCodes(dto)
|
||||||
|
|
||||||
if (isErrorResponse(result)) {
|
if (isErrorResponse(result)) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ApplicationServiceInterface } from './../Service/ApplicationServiceInterface'
|
import { ApplicationServiceInterface } from './../Service/ApplicationServiceInterface'
|
||||||
import { DecryptedItem, DecryptedItemInterface, FileItem, SNNote } from '@standardnotes/models'
|
import { DecryptedItem, DecryptedItemInterface, FileItem, SNNote } from '@standardnotes/models'
|
||||||
import { ChallengeInterface, ChallengeReason } from '../Challenge'
|
import { ChallengeInterface, ChallengeReason, ChallengeResponseInterface } from '../Challenge'
|
||||||
import { MobileUnlockTiming } from './MobileUnlockTiming'
|
import { MobileUnlockTiming } from './MobileUnlockTiming'
|
||||||
import { TimingDisplayOption } from './TimingDisplayOption'
|
import { TimingDisplayOption } from './TimingDisplayOption'
|
||||||
import { ProtectionEvent } from './ProtectionEvent'
|
import { ProtectionEvent } from './ProtectionEvent'
|
||||||
@@ -28,6 +28,10 @@ export interface ProtectionsClientInterface extends ApplicationServiceInterface<
|
|||||||
reason: ChallengeReason,
|
reason: ChallengeReason,
|
||||||
dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean },
|
dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean },
|
||||||
): Promise<boolean>
|
): Promise<boolean>
|
||||||
|
authorizeActionWithChallengeResponse(
|
||||||
|
reason: ChallengeReason,
|
||||||
|
dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean },
|
||||||
|
): Promise<{ success: boolean; challengeResponse?: ChallengeResponseInterface }>
|
||||||
authorizeAddingPasscode(): Promise<boolean>
|
authorizeAddingPasscode(): Promise<boolean>
|
||||||
authorizeRemovingPasscode(): Promise<boolean>
|
authorizeRemovingPasscode(): Promise<boolean>
|
||||||
authorizeChangingPasscode(): Promise<boolean>
|
authorizeChangingPasscode(): Promise<boolean>
|
||||||
@@ -36,7 +40,8 @@ export interface ProtectionsClientInterface extends ApplicationServiceInterface<
|
|||||||
authorizeAutolockIntervalChange(): Promise<boolean>
|
authorizeAutolockIntervalChange(): Promise<boolean>
|
||||||
authorizeSearchingProtectedNotesText(): Promise<boolean>
|
authorizeSearchingProtectedNotesText(): Promise<boolean>
|
||||||
authorizeBackupCreation(): Promise<boolean>
|
authorizeBackupCreation(): Promise<boolean>
|
||||||
authorizeMfaDisable(): Promise<boolean>
|
authorizeMfaDisable(): Promise<{ success: boolean; challengeResponse?: ChallengeResponseInterface }>
|
||||||
|
authorizeAccountDeletion(): Promise<{ success: boolean; challengeResponse?: ChallengeResponseInterface }>
|
||||||
|
|
||||||
protectItems<I extends DecryptedItemInterface>(items: I[]): Promise<I[]>
|
protectItems<I extends DecryptedItemInterface>(items: I[]): Promise<I[]>
|
||||||
unprotectItems<I extends DecryptedItemInterface>(items: I[], reason: ChallengeReason): Promise<I[] | undefined>
|
unprotectItems<I extends DecryptedItemInterface>(items: I[], reason: ChallengeReason): Promise<I[] | undefined>
|
||||||
|
|||||||
@@ -237,13 +237,9 @@ export class UserService
|
|||||||
error: boolean
|
error: boolean
|
||||||
message?: string
|
message?: string
|
||||||
}> {
|
}> {
|
||||||
if (
|
const { success, challengeResponse } = await this.protections.authorizeAccountDeletion()
|
||||||
!(await this.protections.authorizeAction(ChallengeReason.DeleteAccount, {
|
|
||||||
fallBackToAccountPassword: true,
|
if (!success) {
|
||||||
requireAccountPassword: true,
|
|
||||||
forcePrompt: false,
|
|
||||||
}))
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
error: true,
|
error: true,
|
||||||
message: Messages.INVALID_PASSWORD,
|
message: Messages.INVALID_PASSWORD,
|
||||||
@@ -251,7 +247,13 @@ export class UserService
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uuid = this.sessions.getSureUser().uuid
|
const uuid = this.sessions.getSureUser().uuid
|
||||||
const response = await this.userApi.deleteAccount(uuid)
|
const password = challengeResponse?.getValueForType(ChallengeValidation.AccountPassword).value as string
|
||||||
|
const currentRootKey = await this.encryption.computeRootKey(
|
||||||
|
password,
|
||||||
|
this.encryption.getRootKeyParams() as SNRootKeyParams,
|
||||||
|
)
|
||||||
|
const serverPassword = currentRootKey.serverPassword
|
||||||
|
const response = await this.userApi.deleteAccount({ userUuid: uuid, serverPassword: serverPassword })
|
||||||
if (isErrorResponse(response)) {
|
if (isErrorResponse(response)) {
|
||||||
return {
|
return {
|
||||||
error: true,
|
error: true,
|
||||||
|
|||||||
@@ -1026,6 +1026,7 @@ export class Dependencies {
|
|||||||
return new GetRecoveryCodes(
|
return new GetRecoveryCodes(
|
||||||
this.get<AuthManager>(TYPES.AuthManager),
|
this.get<AuthManager>(TYPES.AuthManager),
|
||||||
this.get<SettingsService>(TYPES.SettingsService),
|
this.get<SettingsService>(TYPES.SettingsService),
|
||||||
|
this.get<EncryptionService>(TYPES.EncryptionService),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1231,6 +1232,7 @@ export class Dependencies {
|
|||||||
this.get<PureCryptoInterface>(TYPES.Crypto),
|
this.get<PureCryptoInterface>(TYPES.Crypto),
|
||||||
this.get<FeaturesService>(TYPES.FeaturesService),
|
this.get<FeaturesService>(TYPES.FeaturesService),
|
||||||
this.get<ProtectionsClientInterface>(TYPES.ProtectionService),
|
this.get<ProtectionsClientInterface>(TYPES.ProtectionService),
|
||||||
|
this.get<EncryptionService>(TYPES.EncryptionService),
|
||||||
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AuthClientInterface } from '@standardnotes/services'
|
import { AuthClientInterface, EncryptionService } from '@standardnotes/services'
|
||||||
import { SettingsClientInterface } from '@Lib/Services/Settings/SettingsClientInterface'
|
import { SettingsClientInterface } from '@Lib/Services/Settings/SettingsClientInterface'
|
||||||
|
|
||||||
import { GetRecoveryCodes } from './GetRecoveryCodes'
|
import { GetRecoveryCodes } from './GetRecoveryCodes'
|
||||||
@@ -6,8 +6,9 @@ import { GetRecoveryCodes } from './GetRecoveryCodes'
|
|||||||
describe('GetRecoveryCodes', () => {
|
describe('GetRecoveryCodes', () => {
|
||||||
let authClient: AuthClientInterface
|
let authClient: AuthClientInterface
|
||||||
let settingsClient: SettingsClientInterface
|
let settingsClient: SettingsClientInterface
|
||||||
|
let encryption: EncryptionService
|
||||||
|
|
||||||
const createUseCase = () => new GetRecoveryCodes(authClient, settingsClient)
|
const createUseCase = () => new GetRecoveryCodes(authClient, settingsClient, encryption)
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
authClient = {} as jest.Mocked<AuthClientInterface>
|
authClient = {} as jest.Mocked<AuthClientInterface>
|
||||||
@@ -15,12 +16,16 @@ describe('GetRecoveryCodes', () => {
|
|||||||
|
|
||||||
settingsClient = {} as jest.Mocked<SettingsClientInterface>
|
settingsClient = {} as jest.Mocked<SettingsClientInterface>
|
||||||
settingsClient.getSetting = jest.fn().mockResolvedValue('existing-recovery-codes')
|
settingsClient.getSetting = jest.fn().mockResolvedValue('existing-recovery-codes')
|
||||||
|
|
||||||
|
encryption = {} as jest.Mocked<EncryptionService>
|
||||||
|
encryption.computeRootKey = jest.fn().mockResolvedValue({ serverPassword: 'test-server-password' })
|
||||||
|
encryption.getRootKeyParams = jest.fn().mockReturnValue({ algorithm: 'test-algorithm' })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return existing recovery code if they exist', async () => {
|
it('should return existing recovery code if they exist', async () => {
|
||||||
const useCase = createUseCase()
|
const useCase = createUseCase()
|
||||||
|
|
||||||
const result = await useCase.execute()
|
const result = await useCase.execute({ password: 'test-password' })
|
||||||
|
|
||||||
expect(result.getValue()).toBe('existing-recovery-codes')
|
expect(result.getValue()).toBe('existing-recovery-codes')
|
||||||
})
|
})
|
||||||
@@ -30,7 +35,7 @@ describe('GetRecoveryCodes', () => {
|
|||||||
|
|
||||||
const useCase = createUseCase()
|
const useCase = createUseCase()
|
||||||
|
|
||||||
const result = await useCase.execute()
|
const result = await useCase.execute({ password: 'test-password' })
|
||||||
|
|
||||||
expect(result.getValue()).toBe('recovery-codes')
|
expect(result.getValue()).toBe('recovery-codes')
|
||||||
})
|
})
|
||||||
@@ -41,7 +46,7 @@ describe('GetRecoveryCodes', () => {
|
|||||||
|
|
||||||
const useCase = createUseCase()
|
const useCase = createUseCase()
|
||||||
|
|
||||||
const result = await useCase.execute()
|
const result = await useCase.execute({ password: 'test-password' })
|
||||||
|
|
||||||
expect(result.isFailed()).toBe(true)
|
expect(result.isFailed()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,23 +1,40 @@
|
|||||||
import { AuthClientInterface } from '@standardnotes/services'
|
import { AuthClientInterface, EncryptionService } from '@standardnotes/services'
|
||||||
import { Result, SettingName, UseCaseInterface } from '@standardnotes/domain-core'
|
import { Result, SettingName, UseCaseInterface } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
import { SettingsClientInterface } from '@Lib/Services/Settings/SettingsClientInterface'
|
import { SettingsClientInterface } from '@Lib/Services/Settings/SettingsClientInterface'
|
||||||
|
import { GetRecoveryCodesDTO } from './GetRecoveryCodesDTO'
|
||||||
|
import { SNRootKeyParams } from '@standardnotes/encryption'
|
||||||
|
|
||||||
export class GetRecoveryCodes implements UseCaseInterface<string> {
|
export class GetRecoveryCodes implements UseCaseInterface<string> {
|
||||||
constructor(
|
constructor(
|
||||||
private authClient: AuthClientInterface,
|
private authClient: AuthClientInterface,
|
||||||
private settingsClient: SettingsClientInterface,
|
private settingsClient: SettingsClientInterface,
|
||||||
|
private encryption: EncryptionService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(): Promise<Result<string>> {
|
async execute(dto: GetRecoveryCodesDTO): Promise<Result<string>> {
|
||||||
|
if (!dto.password) {
|
||||||
|
return Result.fail('Password is required to get recovery code.')
|
||||||
|
}
|
||||||
|
const currentRootKey = await this.encryption.computeRootKey(
|
||||||
|
dto.password,
|
||||||
|
this.encryption.getRootKeyParams() as SNRootKeyParams,
|
||||||
|
)
|
||||||
|
const serverPassword = currentRootKey.serverPassword
|
||||||
|
|
||||||
|
if (!serverPassword) {
|
||||||
|
return Result.fail('Could not compute server password')
|
||||||
|
}
|
||||||
|
|
||||||
const existingRecoveryCodes = await this.settingsClient.getSetting(
|
const existingRecoveryCodes = await this.settingsClient.getSetting(
|
||||||
SettingName.create(SettingName.NAMES.RecoveryCodes).getValue(),
|
SettingName.create(SettingName.NAMES.RecoveryCodes).getValue(),
|
||||||
|
serverPassword,
|
||||||
)
|
)
|
||||||
if (existingRecoveryCodes !== undefined) {
|
if (existingRecoveryCodes !== undefined) {
|
||||||
return Result.ok(existingRecoveryCodes)
|
return Result.ok(existingRecoveryCodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
const generatedRecoveryCodes = await this.authClient.generateRecoveryCodes()
|
const generatedRecoveryCodes = await this.authClient.generateRecoveryCodes({ serverPassword })
|
||||||
if (generatedRecoveryCodes === false) {
|
if (generatedRecoveryCodes === false) {
|
||||||
return Result.fail('Could not generate recovery code')
|
return Result.fail('Could not generate recovery code')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface GetRecoveryCodesDTO {
|
||||||
|
password?: string
|
||||||
|
}
|
||||||
@@ -578,12 +578,18 @@ export class LegacyApiService
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<GetSettingResponse>> {
|
async getSetting(
|
||||||
|
userUuid: UuidString,
|
||||||
|
settingName: string,
|
||||||
|
serverPassword?: string,
|
||||||
|
): Promise<HttpResponse<GetSettingResponse>> {
|
||||||
|
const customHeaders = serverPassword ? [{ key: 'x-server-password', value: serverPassword }] : undefined
|
||||||
return await this.tokenRefreshableRequest<GetSettingResponse>({
|
return await this.tokenRefreshableRequest<GetSettingResponse>({
|
||||||
verb: HttpVerb.Get,
|
verb: HttpVerb.Get,
|
||||||
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName.toLowerCase())),
|
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName.toLowerCase())),
|
||||||
authentication: this.getSessionAccessToken(),
|
authentication: this.getSessionAccessToken(),
|
||||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||||
|
customHeaders,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,12 +622,18 @@ export class LegacyApiService
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<DeleteSettingResponse>> {
|
async deleteSetting(
|
||||||
|
userUuid: UuidString,
|
||||||
|
settingName: string,
|
||||||
|
serverPassword?: string,
|
||||||
|
): Promise<HttpResponse<DeleteSettingResponse>> {
|
||||||
|
const customHeaders = serverPassword ? [{ key: 'x-server-password', value: serverPassword }] : undefined
|
||||||
return this.tokenRefreshableRequest<DeleteSettingResponse>({
|
return this.tokenRefreshableRequest<DeleteSettingResponse>({
|
||||||
verb: HttpVerb.Delete,
|
verb: HttpVerb.Delete,
|
||||||
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName)),
|
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName)),
|
||||||
authentication: this.getSessionAccessToken(),
|
authentication: this.getSessionAccessToken(),
|
||||||
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||||
|
customHeaders,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import {
|
|||||||
InternalEventBusInterface,
|
InternalEventBusInterface,
|
||||||
MfaServiceInterface,
|
MfaServiceInterface,
|
||||||
ProtectionsClientInterface,
|
ProtectionsClientInterface,
|
||||||
|
EncryptionService,
|
||||||
SignInStrings,
|
SignInStrings,
|
||||||
|
ChallengeValidation,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { SettingName } from '@standardnotes/domain-core'
|
import { SettingName } from '@standardnotes/domain-core'
|
||||||
|
import { SNRootKeyParams } from '@standardnotes/encryption'
|
||||||
|
|
||||||
export class MfaService extends AbstractService implements MfaServiceInterface {
|
export class MfaService extends AbstractService implements MfaServiceInterface {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -16,6 +19,7 @@ export class MfaService extends AbstractService implements MfaServiceInterface {
|
|||||||
private crypto: PureCryptoInterface,
|
private crypto: PureCryptoInterface,
|
||||||
private featuresService: FeaturesService,
|
private featuresService: FeaturesService,
|
||||||
private protections: ProtectionsClientInterface,
|
private protections: ProtectionsClientInterface,
|
||||||
|
private encryption: EncryptionService,
|
||||||
protected override internalEventBus: InternalEventBusInterface,
|
protected override internalEventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(internalEventBus)
|
super(internalEventBus)
|
||||||
@@ -55,11 +59,23 @@ export class MfaService extends AbstractService implements MfaServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async disableMfa(): Promise<void> {
|
async disableMfa(): Promise<void> {
|
||||||
if (!(await this.protections.authorizeMfaDisable())) {
|
const { success, challengeResponse } = await this.protections.authorizeMfaDisable()
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.settingsService.deleteSetting(SettingName.create(SettingName.NAMES.MfaSecret).getValue())
|
const password = challengeResponse?.getValueForType(ChallengeValidation.AccountPassword).value as string
|
||||||
|
const currentRootKey = await this.encryption.computeRootKey(
|
||||||
|
password,
|
||||||
|
this.encryption.getRootKeyParams() as SNRootKeyParams,
|
||||||
|
)
|
||||||
|
const serverPassword = currentRootKey.serverPassword
|
||||||
|
|
||||||
|
return await this.settingsService.deleteSetting(
|
||||||
|
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
|
||||||
|
serverPassword,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override deinit(): void {
|
override deinit(): void {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
import { ContentType } from '@standardnotes/domain-core'
|
import { ContentType } from '@standardnotes/domain-core'
|
||||||
import { isValidProtectionSessionLength } from './isValidProtectionSessionLength'
|
import { isValidProtectionSessionLength } from './isValidProtectionSessionLength'
|
||||||
import { UnprotectedAccessSecondsDuration } from './UnprotectedAccessSecondsDuration'
|
import { UnprotectedAccessSecondsDuration } from './UnprotectedAccessSecondsDuration'
|
||||||
|
import { ChallengeResponse } from '../Challenge'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enforces certain actions to require extra authentication,
|
* Enforces certain actions to require extra authentication,
|
||||||
@@ -246,11 +247,11 @@ export class ProtectionService
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async authorizeMfaDisable(): Promise<boolean> {
|
async authorizeMfaDisable(): Promise<{ success: boolean; challengeResponse?: ChallengeResponse }> {
|
||||||
return this.authorizeAction(ChallengeReason.DisableMfa, {
|
return this.authorizeActionWithChallengeResponse(ChallengeReason.DisableMfa, {
|
||||||
fallBackToAccountPassword: true,
|
fallBackToAccountPassword: true,
|
||||||
requireAccountPassword: true,
|
requireAccountPassword: true,
|
||||||
forcePrompt: false,
|
forcePrompt: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +279,14 @@ export class ProtectionService
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async authorizeAccountDeletion(): Promise<{ success: boolean; challengeResponse?: ChallengeResponse }> {
|
||||||
|
return this.authorizeActionWithChallengeResponse(ChallengeReason.DeleteAccount, {
|
||||||
|
fallBackToAccountPassword: true,
|
||||||
|
requireAccountPassword: true,
|
||||||
|
forcePrompt: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async authorizeAction(
|
async authorizeAction(
|
||||||
reason: ChallengeReason,
|
reason: ChallengeReason,
|
||||||
dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean },
|
dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean },
|
||||||
@@ -285,6 +294,13 @@ export class ProtectionService
|
|||||||
return this.validateOrRenewSession(reason, dto)
|
return this.validateOrRenewSession(reason, dto)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async authorizeActionWithChallengeResponse(
|
||||||
|
reason: ChallengeReason,
|
||||||
|
dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean },
|
||||||
|
): Promise<{ success: boolean; challengeResponse?: ChallengeResponse }> {
|
||||||
|
return this.validateOrRenewSessionWithChallengeResponse(reason, dto)
|
||||||
|
}
|
||||||
|
|
||||||
getMobilePasscodeTimingOptions(): TimingDisplayOption[] {
|
getMobilePasscodeTimingOptions(): TimingDisplayOption[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -353,8 +369,20 @@ export class ProtectionService
|
|||||||
reason: ChallengeReason,
|
reason: ChallengeReason,
|
||||||
{ fallBackToAccountPassword = true, requireAccountPassword = false, forcePrompt = false } = {},
|
{ fallBackToAccountPassword = true, requireAccountPassword = false, forcePrompt = false } = {},
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const response = await this.validateOrRenewSessionWithChallengeResponse(reason, {
|
||||||
|
fallBackToAccountPassword,
|
||||||
|
requireAccountPassword,
|
||||||
|
forcePrompt,
|
||||||
|
})
|
||||||
|
return response.success
|
||||||
|
}
|
||||||
|
|
||||||
|
private async validateOrRenewSessionWithChallengeResponse(
|
||||||
|
reason: ChallengeReason,
|
||||||
|
{ fallBackToAccountPassword = true, requireAccountPassword = false, forcePrompt = false } = {},
|
||||||
|
): Promise<{ success: boolean; challengeResponse?: ChallengeResponse }> {
|
||||||
if (this.getSessionExpiryDate() > new Date() && !forcePrompt) {
|
if (this.getSessionExpiryDate() > new Date() && !forcePrompt) {
|
||||||
return true
|
return { success: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompts: ChallengePrompt[] = []
|
const prompts: ChallengePrompt[] = []
|
||||||
@@ -378,9 +406,10 @@ export class ProtectionService
|
|||||||
if (fallBackToAccountPassword && this.encryption.hasAccount()) {
|
if (fallBackToAccountPassword && this.encryption.hasAccount()) {
|
||||||
prompts.push(new ChallengePrompt(ChallengeValidation.AccountPassword))
|
prompts.push(new ChallengePrompt(ChallengeValidation.AccountPassword))
|
||||||
} else {
|
} else {
|
||||||
return true
|
return { success: true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastSessionLength = this.getLastSessionLength()
|
const lastSessionLength = this.getLastSessionLength()
|
||||||
const chosenSessionLength = isValidProtectionSessionLength(lastSessionLength)
|
const chosenSessionLength = isValidProtectionSessionLength(lastSessionLength)
|
||||||
? lastSessionLength
|
? lastSessionLength
|
||||||
@@ -407,9 +436,9 @@ export class ProtectionService
|
|||||||
} else {
|
} else {
|
||||||
this.setSessionLength(length as UnprotectedAccessSecondsDuration)
|
this.setSessionLength(length as UnprotectedAccessSecondsDuration)
|
||||||
}
|
}
|
||||||
return true
|
return { success: true, challengeResponse: response }
|
||||||
} else {
|
} else {
|
||||||
return false
|
return { success: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export class SettingsService extends AbstractService implements SettingsClientIn
|
|||||||
return this.provider.listSettings()
|
return this.provider.listSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSetting(name: SettingName) {
|
async getSetting(name: SettingName, serverPassword?: string) {
|
||||||
return this.provider.getSetting(name)
|
return this.provider.getSetting(name, serverPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSubscriptionSetting(name: SettingName) {
|
async getSubscriptionSetting(name: SettingName) {
|
||||||
@@ -50,8 +50,8 @@ export class SettingsService extends AbstractService implements SettingsClientIn
|
|||||||
return this.provider.getDoesSensitiveSettingExist(name)
|
return this.provider.getDoesSensitiveSettingExist(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteSetting(name: SettingName) {
|
async deleteSetting(name: SettingName, serverPassword?: string) {
|
||||||
return this.provider.deleteSetting(name)
|
return this.provider.deleteSetting(name, serverPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
getEmailBackupFrequencyOptionLabel(frequency: EmailBackupFrequency): string {
|
getEmailBackupFrequencyOptionLabel(frequency: EmailBackupFrequency): string {
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { SettingName } from '@standardnotes/domain-core'
|
|||||||
export interface SettingsClientInterface {
|
export interface SettingsClientInterface {
|
||||||
listSettings(): Promise<SettingsList>
|
listSettings(): Promise<SettingsList>
|
||||||
|
|
||||||
getSetting(name: SettingName): Promise<string | undefined>
|
getSetting(name: SettingName, serverPassword?: string): Promise<string | undefined>
|
||||||
|
|
||||||
getDoesSensitiveSettingExist(name: SettingName): Promise<boolean>
|
getDoesSensitiveSettingExist(name: SettingName): Promise<boolean>
|
||||||
|
|
||||||
updateSetting(name: SettingName, payload: string, sensitive?: boolean): Promise<void>
|
updateSetting(name: SettingName, payload: string, sensitive?: boolean): Promise<void>
|
||||||
|
|
||||||
deleteSetting(name: SettingName): Promise<void>
|
deleteSetting(name: SettingName, serverPassword?: string): Promise<void>
|
||||||
|
|
||||||
getEmailBackupFrequencyOptionLabel(frequency: EmailBackupFrequency): string
|
getEmailBackupFrequencyOptionLabel(frequency: EmailBackupFrequency): string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ export class SettingsGateway {
|
|||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSetting(name: SettingName): Promise<string | undefined> {
|
async getSetting(name: SettingName, serverPassword?: string): Promise<string | undefined> {
|
||||||
const response = await this.settingsApi.getSetting(this.userUuid, name.value)
|
const response = await this.settingsApi.getSetting(this.userUuid, name.value, serverPassword)
|
||||||
|
|
||||||
if (response.status === HttpStatusCode.BadRequest) {
|
if (response.status === HttpStatusCode.BadRequest) {
|
||||||
return undefined
|
return undefined
|
||||||
@@ -109,8 +109,8 @@ export class SettingsGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteSetting(name: SettingName): Promise<void> {
|
async deleteSetting(name: SettingName, serverPassword?: string): Promise<void> {
|
||||||
const response = await this.settingsApi.deleteSetting(this.userUuid, name.value)
|
const response = await this.settingsApi.deleteSetting(this.userUuid, name.value, serverPassword)
|
||||||
if (isErrorResponse(response)) {
|
if (isErrorResponse(response)) {
|
||||||
throw new Error(getErrorFromErrorResponse(response).message)
|
throw new Error(getErrorFromErrorResponse(response).message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ export interface SettingsServerInterface {
|
|||||||
sensitive: boolean,
|
sensitive: boolean,
|
||||||
): Promise<HttpResponse<UpdateSettingResponse>>
|
): Promise<HttpResponse<UpdateSettingResponse>>
|
||||||
|
|
||||||
getSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<GetSettingResponse>>
|
getSetting(
|
||||||
|
userUuid: UuidString,
|
||||||
|
settingName: string,
|
||||||
|
serverPassword?: string,
|
||||||
|
): Promise<HttpResponse<GetSettingResponse>>
|
||||||
|
|
||||||
getSubscriptionSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<GetSettingResponse>>
|
getSubscriptionSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<GetSettingResponse>>
|
||||||
|
|
||||||
@@ -28,5 +32,9 @@ export interface SettingsServerInterface {
|
|||||||
sensitive: boolean,
|
sensitive: boolean,
|
||||||
): Promise<HttpResponse<UpdateSettingResponse>>
|
): Promise<HttpResponse<UpdateSettingResponse>>
|
||||||
|
|
||||||
deleteSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<DeleteSettingResponse>>
|
deleteSetting(
|
||||||
|
userUuid: UuidString,
|
||||||
|
settingName: string,
|
||||||
|
serverPassword?: string,
|
||||||
|
): Promise<HttpResponse<DeleteSettingResponse>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -549,6 +549,24 @@ describe('basic auth', function () {
|
|||||||
expect(sendChallengeSpy.callCount).to.equal(1)
|
expect(sendChallengeSpy.callCount).to.equal(1)
|
||||||
}).timeout(Factory.TenSecondTimeout)
|
}).timeout(Factory.TenSecondTimeout)
|
||||||
|
|
||||||
|
it('should send server password when deleting account', async function () {
|
||||||
|
Factory.handlePasswordChallenges(context.application, context.password)
|
||||||
|
|
||||||
|
const userApiService = context.application.dependencies.get(TYPES.UserApiService)
|
||||||
|
const deleteAccountSpy = sinon.spy(userApiService, 'deleteAccount')
|
||||||
|
|
||||||
|
await context.application.user.deleteAccount()
|
||||||
|
|
||||||
|
expect(deleteAccountSpy.callCount).to.equal(1)
|
||||||
|
const deleteAccountCall = deleteAccountSpy.getCall(0)
|
||||||
|
const callArgs = deleteAccountCall.args[0]
|
||||||
|
|
||||||
|
expect(callArgs).to.have.property('serverPassword')
|
||||||
|
expect(callArgs.serverPassword).to.not.be.undefined
|
||||||
|
expect(typeof callArgs.serverPassword).to.equal('string')
|
||||||
|
expect(callArgs.serverPassword.length).to.be.above(0)
|
||||||
|
}).timeout(Factory.TenSecondTimeout)
|
||||||
|
|
||||||
it('deleting account should sign out current user', async function () {
|
it('deleting account should sign out current user', async function () {
|
||||||
Factory.handlePasswordChallenges(context.application, context.password)
|
Factory.handlePasswordChallenges(context.application, context.password)
|
||||||
|
|
||||||
@@ -567,12 +585,40 @@ describe('basic auth', function () {
|
|||||||
|
|
||||||
const response = await context.application.dependencies
|
const response = await context.application.dependencies
|
||||||
.get(TYPES.UserApiService)
|
.get(TYPES.UserApiService)
|
||||||
.deleteAccount(registerResponse.user.uuid)
|
.deleteAccount({
|
||||||
|
userUuid: registerResponse.user.uuid,
|
||||||
|
serverPassword: 'dummy-password'
|
||||||
|
})
|
||||||
|
|
||||||
expect(response.status).to.equal(401)
|
expect(response.status).to.equal(401)
|
||||||
expect(response.data.error.message).to.equal('Operation not allowed.')
|
expect(response.data.error.message).to.equal('Operation not allowed.')
|
||||||
|
|
||||||
await secondContext.deinit()
|
await secondContext.deinit()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should not allow deleting account if server password is not sent', async function () {
|
||||||
|
Factory.handlePasswordChallenges(context.application, context.password)
|
||||||
|
|
||||||
|
const response = await context.application.dependencies
|
||||||
|
.get(TYPES.UserApiService)
|
||||||
|
.deleteAccount({
|
||||||
|
userUuid: context.application.user.uuid,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).to.equal(400)
|
||||||
|
}).timeout(Factory.TenSecondTimeout)
|
||||||
|
|
||||||
|
it('should not allow deleting account if server password is incorrect', async function () {
|
||||||
|
Factory.handlePasswordChallenges(context.application, context.password)
|
||||||
|
|
||||||
|
const response = await context.application.dependencies
|
||||||
|
.get(TYPES.UserApiService)
|
||||||
|
.deleteAccount({
|
||||||
|
userUuid: context.application.user.uuid,
|
||||||
|
serverPassword: 'wrong-password'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).to.equal(400)
|
||||||
|
}).timeout(Factory.TenSecondTimeout)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ describe('mfa service', () => {
|
|||||||
const token = await application.mfa.getOtpToken(secret)
|
const token = await application.mfa.getOtpToken(secret)
|
||||||
|
|
||||||
sinon.spy(application.challenges, 'sendChallenge')
|
sinon.spy(application.challenges, 'sendChallenge')
|
||||||
|
|
||||||
await application.mfa.enableMfa(secret, token)
|
await application.mfa.enableMfa(secret, token)
|
||||||
await application.mfa.disableMfa()
|
await application.mfa.disableMfa()
|
||||||
|
|
||||||
@@ -73,4 +74,64 @@ describe('mfa service', () => {
|
|||||||
expect(challenge.prompts).to.have.lengthOf(2)
|
expect(challenge.prompts).to.have.lengthOf(2)
|
||||||
expect(challenge.prompts[0].validation).to.equal(ChallengeValidation.AccountPassword)
|
expect(challenge.prompts[0].validation).to.equal(ChallengeValidation.AccountPassword)
|
||||||
}).timeout(Factory.TenSecondTimeout)
|
}).timeout(Factory.TenSecondTimeout)
|
||||||
|
|
||||||
|
it('sends server password when disabling mfa', async () => {
|
||||||
|
await registerApp(application)
|
||||||
|
|
||||||
|
Factory.handlePasswordChallenges(application, accountPassword)
|
||||||
|
const secret = await application.mfa.generateMfaSecret()
|
||||||
|
const token = await application.mfa.getOtpToken(secret)
|
||||||
|
|
||||||
|
await application.mfa.enableMfa(secret, token)
|
||||||
|
|
||||||
|
sinon.spy(application.settings.settingsApi, 'deleteSetting')
|
||||||
|
|
||||||
|
await application.mfa.disableMfa()
|
||||||
|
|
||||||
|
const deleteSettingCall = application.settings.settingsApi.deleteSetting.getCall(0)
|
||||||
|
const [serverPassword] = deleteSettingCall.args
|
||||||
|
expect(typeof serverPassword).to.equal('string')
|
||||||
|
expect(serverPassword.length).to.be.above(0)
|
||||||
|
}).timeout(Factory.TenSecondTimeout)
|
||||||
|
|
||||||
|
it('should not allow disabling mfa if server password is not sent', async function () {
|
||||||
|
await registerApp(application)
|
||||||
|
|
||||||
|
Factory.handlePasswordChallenges(application, accountPassword)
|
||||||
|
|
||||||
|
const secret = await application.mfa.generateMfaSecret()
|
||||||
|
const token = await application.mfa.getOtpToken(secret)
|
||||||
|
|
||||||
|
await application.mfa.enableMfa(secret, token)
|
||||||
|
|
||||||
|
const response = await application.dependencies
|
||||||
|
.get(TYPES.SettingsApiService)
|
||||||
|
.deleteSetting({
|
||||||
|
userUuid: application.user.uuid,
|
||||||
|
settingName: 'MFA_SECRET',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).to.equal(400)
|
||||||
|
}).timeout(Factory.TenSecondTimeout)
|
||||||
|
|
||||||
|
it('should not allow disabling mfa if server password is incorrect', async function () {
|
||||||
|
await registerApp(application)
|
||||||
|
|
||||||
|
Factory.handlePasswordChallenges(application, accountPassword)
|
||||||
|
|
||||||
|
const secret = await application.mfa.generateMfaSecret()
|
||||||
|
const token = await application.mfa.getOtpToken(secret)
|
||||||
|
|
||||||
|
await application.mfa.enableMfa(secret, token)
|
||||||
|
|
||||||
|
const response = await application.dependencies
|
||||||
|
.get(TYPES.SettingsApiService)
|
||||||
|
.deleteSetting({
|
||||||
|
userUuid: application.user.uuid,
|
||||||
|
settingName: 'MFA_SECRET',
|
||||||
|
serverPassword: 'wrong-password'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).to.equal(400)
|
||||||
|
}).timeout(Factory.TenSecondTimeout)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ const RecoveryCodeBanner = ({ application }: { application: WebApplication }) =>
|
|||||||
const [errorMessage, setErrorMessage] = useState<string>()
|
const [errorMessage, setErrorMessage] = useState<string>()
|
||||||
|
|
||||||
const onClickShow = async () => {
|
const onClickShow = async () => {
|
||||||
const authorized = await application.challenges.promptForAccountPassword()
|
const password = await application.challenges.promptForAccountPassword()
|
||||||
|
|
||||||
if (!authorized) {
|
if (!password) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const recoveryCodeOrError = await application.getRecoveryCodes.execute()
|
const recoveryCodeOrError = await application.getRecoveryCodes.execute({ password })
|
||||||
if (recoveryCodeOrError.isFailed()) {
|
if (recoveryCodeOrError.isFailed()) {
|
||||||
setErrorMessage(recoveryCodeOrError.getError())
|
setErrorMessage(recoveryCodeOrError.getError())
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user