1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 14:23:32 +00:00
Files
browser/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts
Andreas Coroiu 725ee08640 [PM-2014] Passkey registration (#5396)
* [PM-2014] feat: scaffold new fido2 login component and module

* [PM-1024] feat: add content to login settings component

* [PM-1024] feat: add badge and button aria label

* [PM-2014] feat: create new dialog

* feat: add ability to remove form field bottom margin

(cherry picked from commit 05925ff77ed47f3865c2aecade8271390d9e2fa6)

* [PM-2014] feat: disable dialog close button

* [PM-2014] feat: implement mocked failing wizard flow

* [PM-2014] feat: add icons and other content

* [PM-2014] feat: change wording to "creating" password

* [PM-2014] feat: add new auth and auth core modules

* [PM-2014] chore: move fido2-login-settings to auth module

* [PM-2014] chore: expose using barrel files

* [PM-2014] feat: fetch webauthn challenge

* [PM-2014] chore: refactor api logic into new api service and move ui logic into existing service

* [PM-2014] feat: add tests for new credential options

* [PM-2014] feat: return undefined when credential creation fails

* [PM-2014] feat: implement credential creation

* [PM-2014] feat: add passkey naming ui

* [PM-2014] feat: add support for creation token

* [PM-2014] feat: implement credential saving

* [PM-2014] feat: Basic list of credentials

* [PM-2014] feat: improve async data loading

* [PM-2014] feat: finish up list UI

* [PM-2014] fix: loading state not being set properly

* [PM-2014] feat: improve aria labels

* [PM-2014] feat: show toast on passkey saved

* [PM-2014] feat: add delete dialog

* [PM-2014] feat: implement deletion without user verification

* [PM-2014] feat: add user verification to delete

* [PM-2014] feat: change to danger button

* [PM-2014] feat: show `save` if passkeys already exist

* [PM-2014] feat: add passkey limit

* [PM-2014] feat: improve error on delete

* [PM-2014] feat: add support for feature flag

* [PM-2014] feat: update copy

* [PM-2014] feat: reduce remove button margin

* [PM-2014] feat: refactor submit method

* [PM-2014] feat: autofocus fields

* [PM-2014] fix: move error handling to components

After discussing it with Jake we decided that following convention was best.

* [PM-2014] feat: change toast depending on existing passkeys

* [PM-2014] chore: rename everything from `fido2` to `webauthn`

* [PM-2014] fix: `CoreAuthModule` duplicate import

* [PM-2014] feat: change to new figma design `Encryption not supported`

* [PM-2014] fix: add missing href

* [PM-2014] fix: misaligned badge

* [PM-2014] chore: remove whitespace

* [PM-2014] fix: dialog close bug

* [PM-2014] fix: badge alignment not applying properly

* [PM-2014] fix: remove redundant align class

* [PM-2014] chore: move CoreAuthModule to AuthModule

* [PM-2014] feat: create new settings module

* [PM-2014] feat: move change password component to settings module

* [PM-2014] chore: tweak loose components recommendation

* [PM-2014] fix: remove deprecated pattern

* [PM-2014] chore: rename everything to `WebauthnLogin` to follow new naming scheme

* [PM-2014] chore: document requests and responses

* [PM-2014] fix: remove `undefined`

* [PM-2014] fix: clarify webauthn login service

* [PM-2014] fix: use `getCredentials$()`

* [PM-2014] fix: badge alignment using important statement

* [PM-2014] fix: remove sm billing flag

* [PM-2014] fix: `CoreAuthModule` double import

* [PM-2014] fix: unimported component (issue due to conflict with master)

* [PM-2014] fix: unawaited promise bug
2023-10-10 15:10:26 +02:00

179 lines
5.7 KiB
TypeScript

import { DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { firstValueFrom, map, Observable } from "rxjs";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";
import { WebauthnLoginService } from "../../../core";
import { CredentialCreateOptionsView } from "../../../core/views/credential-create-options.view";
import { CreatePasskeyFailedIcon } from "./create-passkey-failed.icon";
import { CreatePasskeyIcon } from "./create-passkey.icon";
export enum CreateCredentialDialogResult {
Success,
}
type Step =
| "userVerification"
| "credentialCreation"
| "credentialCreationFailed"
| "credentialNaming";
@Component({
templateUrl: "create-credential-dialog.component.html",
})
export class CreateCredentialDialogComponent implements OnInit {
protected readonly NameMaxCharacters = 50;
protected readonly CreateCredentialDialogResult = CreateCredentialDialogResult;
protected readonly Icons = { CreatePasskeyIcon, CreatePasskeyFailedIcon };
protected currentStep: Step = "userVerification";
protected formGroup = this.formBuilder.group({
userVerification: this.formBuilder.group({
masterPassword: ["", [Validators.required]],
}),
credentialNaming: this.formBuilder.group({
name: ["", Validators.maxLength(50)],
}),
});
protected credentialOptions?: CredentialCreateOptionsView;
protected deviceResponse?: PublicKeyCredential;
protected hasPasskeys$?: Observable<boolean>;
constructor(
private formBuilder: FormBuilder,
private dialogRef: DialogRef,
private webauthnService: WebauthnLoginService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private logService: LogService
) {}
ngOnInit(): void {
this.hasPasskeys$ = this.webauthnService
.getCredentials$()
.pipe(map((credentials) => credentials.length > 0));
}
protected submit = async () => {
this.dialogRef.disableClose = true;
try {
switch (this.currentStep) {
case "userVerification":
return await this.submitUserVerification();
case "credentialCreationFailed":
return await this.submitCredentialCreationFailed();
case "credentialCreation":
return await this.submitCredentialCreation();
case "credentialNaming":
return await this.submitCredentialNaming();
}
} finally {
this.dialogRef.disableClose = false;
}
};
protected async submitUserVerification() {
this.formGroup.controls.userVerification.markAllAsTouched();
if (this.formGroup.controls.userVerification.invalid) {
return;
}
try {
this.credentialOptions = await this.webauthnService.getCredentialCreateOptions({
type: VerificationType.MasterPassword,
secret: this.formGroup.value.userVerification.masterPassword,
});
} catch (error) {
if (error instanceof ErrorResponse && error.statusCode === 400) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("error"),
this.i18nService.t("invalidMasterPassword")
);
} else {
this.logService?.error(error);
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
}
return;
}
this.currentStep = "credentialCreation";
await this.submitCredentialCreation();
}
protected async submitCredentialCreation() {
this.deviceResponse = await this.webauthnService.createCredential(this.credentialOptions);
if (this.deviceResponse === undefined) {
this.currentStep = "credentialCreationFailed";
return;
}
this.currentStep = "credentialNaming";
}
protected async submitCredentialCreationFailed() {
this.currentStep = "credentialCreation";
await this.submitCredentialCreation();
}
protected async submitCredentialNaming() {
this.formGroup.controls.credentialNaming.markAllAsTouched();
if (this.formGroup.controls.credentialNaming.invalid) {
return;
}
const name = this.formGroup.value.credentialNaming.name;
try {
await this.webauthnService.saveCredential(
this.credentialOptions,
this.deviceResponse,
this.formGroup.value.credentialNaming.name
);
} catch (error) {
this.logService?.error(error);
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
return;
}
if (await firstValueFrom(this.hasPasskeys$)) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("passkeySaved", name)
);
} else {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("loginWithPasskeyEnabled")
);
}
this.dialogRef.close(CreateCredentialDialogResult.Success);
}
}
/**
* Strongly typed helper to open a CreateCredentialDialog
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
export const openCreateCredentialDialog = (
dialogService: DialogService,
config: DialogConfig<unknown>
) => {
return dialogService.open<CreateCredentialDialogResult | undefined, unknown>(
CreateCredentialDialogComponent,
config
);
};