diff --git a/apps/web/src/app/auth/core/services/webauthn/webauthn.service.spec.ts b/apps/web/src/app/auth/core/services/webauthn/webauthn.service.spec.ts index e69de29bb2..5453e8c328 100644 --- a/apps/web/src/app/auth/core/services/webauthn/webauthn.service.spec.ts +++ b/apps/web/src/app/auth/core/services/webauthn/webauthn.service.spec.ts @@ -0,0 +1,53 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; +import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; +import { ChallengeResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response"; +import { Verification } from "@bitwarden/common/types/verification"; + +import { WebauthnApiService } from "./webauthn-api.service"; +import { WebauthnService } from "./webauthn.service"; + +describe("WebauthnService", () => { + let apiService!: MockProxy; + let platformUtilsService!: MockProxy; + let i18nService!: MockProxy; + let webauthnService!: WebauthnService; + + beforeAll(() => { + apiService = mock(); + platformUtilsService = mock(); + i18nService = mock(); + webauthnService = new WebauthnService(apiService, platformUtilsService, i18nService); + }); + + describe("getNewCredentialOptions", () => { + it("should return undefined and show toast when api service call throws", async () => { + apiService.getChallenge.mockRejectedValue(new Error("Mock error")); + const verification = createVerification(); + + const result = await webauthnService.getNewCredentialOptions(verification); + + expect(result).toBeUndefined(); + expect(platformUtilsService.showToast).toHaveBeenCalled(); + }); + + it("should return options when api service call is successfull", async () => { + const challenge: ChallengeResponse = Symbol() as any; + apiService.getChallenge.mockResolvedValue(challenge); + const verification = createVerification(); + + const result = await webauthnService.getNewCredentialOptions(verification); + + expect(result).toEqual({ challenge }); + }); + }); +}); + +function createVerification(): Verification { + return { + type: VerificationType.MasterPassword, + secret: "secret", + }; +} diff --git a/apps/web/src/app/auth/core/services/webauthn/webauthn.service.ts b/apps/web/src/app/auth/core/services/webauthn/webauthn.service.ts index 7ef48ef4ec..a65ea868e7 100644 --- a/apps/web/src/app/auth/core/services/webauthn/webauthn.service.ts +++ b/apps/web/src/app/auth/core/services/webauthn/webauthn.service.ts @@ -2,18 +2,16 @@ import { Injectable } from "@angular/core"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; -import { ChallengeResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { Verification } from "@bitwarden/common/types/verification"; import { CoreAuthModule } from "../../core.module"; +import { NewCredentialOptionsView } from "../../views/new-credential-options.view"; import { WebauthnApiService } from "./webauthn-api.service"; type WebauthnCredentialView = unknown; -export class UserVerificationFailedError extends Error {} - @Injectable({ providedIn: CoreAuthModule }) export class WebauthnService { constructor( @@ -22,9 +20,9 @@ export class WebauthnService { private i18nService: I18nService ) {} - async newCredentialOptions(verification: Verification): Promise { + async getNewCredentialOptions(verification: Verification): Promise { try { - return await this.apiService.getChallenge(verification); + return { challenge: await this.apiService.getChallenge(verification) }; } catch (error) { if (error instanceof ErrorResponse && error.statusCode === 400) { this.platformUtilsService.showToast( @@ -39,7 +37,9 @@ export class WebauthnService { } } - async createCredential(challenge: ChallengeResponse): Promise { + async createCredential( + credentialOptions: NewCredentialOptionsView + ): Promise { await new Promise((_, reject) => setTimeout(() => reject(new Error("Not implemented")), 1000)); return undefined; } diff --git a/apps/web/src/app/auth/core/views/new-credential-options.view.ts b/apps/web/src/app/auth/core/views/new-credential-options.view.ts new file mode 100644 index 0000000000..5cfe70b26c --- /dev/null +++ b/apps/web/src/app/auth/core/views/new-credential-options.view.ts @@ -0,0 +1,5 @@ +import { ChallengeResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response"; + +export class NewCredentialOptionsView { + constructor(readonly challenge: ChallengeResponse) {} +} diff --git a/apps/web/src/app/auth/settings/fido2-login-settings/create-credential-dialog/create-credential-dialog.component.ts b/apps/web/src/app/auth/settings/fido2-login-settings/create-credential-dialog/create-credential-dialog.component.ts index 494adc1630..b4e7c529fa 100644 --- a/apps/web/src/app/auth/settings/fido2-login-settings/create-credential-dialog/create-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/fido2-login-settings/create-credential-dialog/create-credential-dialog.component.ts @@ -4,9 +4,9 @@ import { FormBuilder, Validators } from "@angular/forms"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; -import { ChallengeResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response"; import { WebauthnService } from "../../../core"; +import { NewCredentialOptionsView } from "../../../core/views/new-credential-options.view"; import { CreatePasskeyFailedIcon } from "./create-passkey-failed.icon"; import { CreatePasskeyIcon } from "./create-passkey.icon"; @@ -38,7 +38,7 @@ export class CreateCredentialDialogComponent { name: ["", Validators.maxLength(50)], }), }); - protected challenge?: ChallengeResponse; + protected credentialOptions?: NewCredentialOptionsView; constructor( private formBuilder: FormBuilder, @@ -56,11 +56,11 @@ export class CreateCredentialDialogComponent { return; } - this.challenge = await this.webauthnService.newCredentialOptions({ + this.credentialOptions = await this.webauthnService.getNewCredentialOptions({ type: VerificationType.MasterPassword, secret: this.formGroup.value.userVerification.masterPassword, }); - if (this.challenge === undefined) { + if (this.credentialOptions === undefined) { return; } this.currentStep = "credentialCreation"; @@ -72,7 +72,7 @@ export class CreateCredentialDialogComponent { if (this.currentStep === "credentialCreation") { try { - await this.webauthnService.createCredential(this.challenge); + await this.webauthnService.createCredential(this.credentialOptions); } catch { this.currentStep = "credentialCreationFailed"; }