diff --git a/apps/web/src/app/auth/core/services/webauthn/response/credential-create-options.response.ts b/apps/web/src/app/auth/core/services/webauthn/response/credential-create-options.response.ts new file mode 100644 index 00000000000..5e258d14ae5 --- /dev/null +++ b/apps/web/src/app/auth/core/services/webauthn/response/credential-create-options.response.ts @@ -0,0 +1,13 @@ +import { ChallengeResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response"; +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class CredentialCreateOptionsResponse extends BaseResponse { + options: ChallengeResponse; + token: string; + + constructor(response: unknown) { + super(response); + this.options = new ChallengeResponse(this.getResponseProperty("options")); + this.token = this.getResponseProperty("token"); + } +} diff --git a/apps/web/src/app/auth/core/services/webauthn/webauthn-api.service.ts b/apps/web/src/app/auth/core/services/webauthn/webauthn-api.service.ts index f422a9b8b7f..04a42d637fd 100644 --- a/apps/web/src/app/auth/core/services/webauthn/webauthn-api.service.ts +++ b/apps/web/src/app/auth/core/services/webauthn/webauthn-api.service.ts @@ -2,11 +2,12 @@ import { Injectable } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; -import { ChallengeResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response"; import { Verification } from "@bitwarden/common/types/verification"; import { CoreAuthModule } from "../../core.module"; +import { CredentialCreateOptionsResponse } from "./response/credential-create-options.response"; + @Injectable({ providedIn: CoreAuthModule }) export class WebauthnApiService { constructor( @@ -14,9 +15,11 @@ export class WebauthnApiService { private userVerificationService: UserVerificationService ) {} - async getChallenge(verification: Verification): Promise { + async getCredentialCreateOptions( + verification: Verification + ): Promise { const request = await this.userVerificationService.buildRequest(verification); const response = await this.apiService.send("POST", "/webauthn/options", request, true, true); - return new ChallengeResponse(response); + return new CredentialCreateOptionsResponse(response); } } 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 92aa3950254..1ceb11273ca 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 @@ -3,11 +3,11 @@ 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 { NewCredentialOptionsView } from "../../views/new-credential-options.view"; +import { CredentialCreateOptionsView } from "../../views/credential-create-options.view"; +import { CredentialCreateOptionsResponse } from "./response/credential-create-options.response"; import { WebauthnApiService } from "./webauthn-api.service"; import { WebauthnService } from "./webauthn.service"; @@ -33,30 +33,32 @@ describe("WebauthnService", () => { describe("getNewCredentialOptions", () => { it("should return undefined and show toast when api service call throws", async () => { - apiService.getChallenge.mockRejectedValue(new Error("Mock error")); + apiService.getCredentialCreateOptions.mockRejectedValue(new Error("Mock error")); const verification = createVerification(); - const result = await webauthnService.getNewCredentialOptions(verification); + const result = await webauthnService.getCredentialCreateOptions(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 options = Symbol() as any; + const token = Symbol() as any; + const response = { options, token } as CredentialCreateOptionsResponse; + apiService.getCredentialCreateOptions.mockResolvedValue(response); const verification = createVerification(); - const result = await webauthnService.getNewCredentialOptions(verification); + const result = await webauthnService.getCredentialCreateOptions(verification); - expect(result).toEqual({ challenge }); + expect(result).toEqual({ options, token }); }); }); describe("createCredential", () => { it("should return undefined when navigator.credentials throws", async () => { credentials.create.mockRejectedValue(new Error("Mocked error")); - const options = createNewCredentialOptions(); + const options = createCredentialCreateOptions(); const result = await webauthnService.createCredential(options); @@ -66,7 +68,7 @@ describe("WebauthnService", () => { it("should return credential when navigator.credentials does not throw", async () => { const credential: Credential = Symbol() as any; credentials.create.mockResolvedValue(credential); - const options = createNewCredentialOptions(); + const options = createCredentialCreateOptions(); const result = await webauthnService.createCredential(options); @@ -82,6 +84,6 @@ function createVerification(): Verification { }; } -function createNewCredentialOptions(): NewCredentialOptionsView { - return new NewCredentialOptionsView(Symbol() as any); +function createCredentialCreateOptions(): CredentialCreateOptionsView { + return new CredentialCreateOptionsView(Symbol() as any, Symbol() as any); } 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 31beea6745d..bea5b414560 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 @@ -1,12 +1,13 @@ import { Injectable, Optional } from "@angular/core"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; 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 { CredentialCreateOptionsView } from "../../views/credential-create-options.view"; import { WebauthnApiService } from "./webauthn-api.service"; @@ -20,17 +21,19 @@ export class WebauthnService { private apiService: WebauthnApiService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, - @Optional() credentials: CredentialsContainer + @Optional() credentials?: CredentialsContainer, + @Optional() private logService?: LogService ) { // Default parameters don't work when used with Angular DI this.credentials = credentials ?? navigator.credentials; } - async getNewCredentialOptions( + async getCredentialCreateOptions( verification: Verification - ): Promise { + ): Promise { try { - return { challenge: await this.apiService.getChallenge(verification) }; + const response = await this.apiService.getCredentialCreateOptions(verification); + return new CredentialCreateOptionsView(response.options, response.token); } catch (error) { if (error instanceof ErrorResponse && error.statusCode === 400) { this.platformUtilsService.showToast( @@ -39,6 +42,7 @@ export class WebauthnService { this.i18nService.t("invalidMasterPassword") ); } else { + this.logService?.error(error); this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError")); } return undefined; @@ -46,10 +50,10 @@ export class WebauthnService { } async createCredential( - credentialOptions: NewCredentialOptionsView + credentialOptions: CredentialCreateOptionsView ): Promise { const nativeOptions: CredentialCreationOptions = { - publicKey: credentialOptions.challenge, + publicKey: credentialOptions.options, }; try { diff --git a/apps/web/src/app/auth/core/views/credential-create-options.view.ts b/apps/web/src/app/auth/core/views/credential-create-options.view.ts new file mode 100644 index 00000000000..29efdef5ee6 --- /dev/null +++ b/apps/web/src/app/auth/core/views/credential-create-options.view.ts @@ -0,0 +1,5 @@ +import { ChallengeResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response"; + +export class CredentialCreateOptionsView { + constructor(readonly options: ChallengeResponse, readonly token: string) {} +} 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 deleted file mode 100644 index 5cfe70b26c5..00000000000 --- a/apps/web/src/app/auth/core/views/new-credential-options.view.ts +++ /dev/null @@ -1,5 +0,0 @@ -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 83a8aa0246a..ed241c2afd5 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 @@ -6,7 +6,7 @@ import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { WebauthnService } from "../../../core"; -import { NewCredentialOptionsView } from "../../../core/views/new-credential-options.view"; +import { CredentialCreateOptionsView } from "../../../core/views/credential-create-options.view"; import { CreatePasskeyFailedIcon } from "./create-passkey-failed.icon"; import { CreatePasskeyIcon } from "./create-passkey.icon"; @@ -39,7 +39,7 @@ export class CreateCredentialDialogComponent { name: ["", Validators.maxLength(50)], }), }); - protected credentialOptions?: NewCredentialOptionsView; + protected credentialOptions?: CredentialCreateOptionsView; constructor( private formBuilder: FormBuilder, @@ -57,7 +57,7 @@ export class CreateCredentialDialogComponent { return; } - this.credentialOptions = await this.webauthnService.getNewCredentialOptions({ + this.credentialOptions = await this.webauthnService.getCredentialCreateOptions({ type: VerificationType.MasterPassword, secret: this.formGroup.value.userVerification.masterPassword, });