mirror of
https://github.com/bitwarden/browser
synced 2026-01-08 03:23:50 +00:00
[PM-2241] Add PRF attestation flow during passkey registration (#6525)
* [PM-2241] chore: refactor into new "pending" view type * [PM-2241] feat: record PRF support * [PM-2241] feat: add prf checkbox to dialog * [PM-2241] chore: remove `disableMargin` instead Will expressed his concern that these things aren't sustainable, and that we should try using `!important` statements instead, which is a good point! * [PM-2241] feat: add prf registration * [PM-2241] feat: add support for `prfStatus` * [PM-2241] feat: add rotateable key set * [PM-2241] feat: add PRF creation error handling * [PM-2241] chore: improve rotateable key docs * [PM-2241] feat: add basic test * [PM-2241] chore: update `SaveCredentialRequest` docs * [PM-2241] chore: rename to `WebauthnLoginAdminService` * [PM-2241] fix: typo in `save-credential.request.ts` * [PM-2241] fix: typo in more places
This commit is contained in:
@@ -32,12 +32,12 @@
|
||||
<p bitTypography="body1">{{ "errorCreatingPasskeyInfo" | i18n }}</p>
|
||||
</div>
|
||||
|
||||
<div *ngIf="currentStep === 'credentialNaming'">
|
||||
<div *ngIf="currentStep === 'credentialNaming'" formGroupName="credentialNaming">
|
||||
<h3 bitTypography="h3">{{ "passkeySuccessfullyCreated" | i18n }}</h3>
|
||||
<p bitTypography="body1">
|
||||
{{ "customPasskeyNameInfo" | i18n }}
|
||||
</p>
|
||||
<bit-form-field disableMargin formGroupName="credentialNaming">
|
||||
<bit-form-field class="!tw-mb-0">
|
||||
<bit-label>{{ "customName" | i18n }}</bit-label>
|
||||
<input type="text" bitInput formControlName="name" appAutofocus />
|
||||
<bit-hint>{{
|
||||
@@ -45,6 +45,11 @@
|
||||
| i18n : formGroup.value.credentialNaming.name.length : NameMaxCharacters
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
<bit-form-control *ngIf="pendingCredential?.supportsPrf" class="!tw-mb-0 tw-mt-6">
|
||||
<input type="checkbox" bitCheckbox formControlName="useForEncryption" />
|
||||
<bit-label>{{ "useForVaultEncryption" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "useForVaultEncryptionInfo" | i18n }}</bit-hint>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { PrfKeySet } from "@bitwarden/auth";
|
||||
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";
|
||||
@@ -10,8 +11,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { Verification } from "@bitwarden/common/types/verification";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { WebauthnLoginService } from "../../../core";
|
||||
import { WebauthnLoginAdminService } from "../../../core";
|
||||
import { CredentialCreateOptionsView } from "../../../core/views/credential-create-options.view";
|
||||
import { PendingWebauthnLoginCredentialView } from "../../../core/views/pending-webauthn-login-credential.view";
|
||||
|
||||
import { CreatePasskeyFailedIcon } from "./create-passkey-failed.icon";
|
||||
import { CreatePasskeyIcon } from "./create-passkey.icon";
|
||||
@@ -42,17 +44,19 @@ export class CreateCredentialDialogComponent implements OnInit {
|
||||
}),
|
||||
credentialNaming: this.formBuilder.group({
|
||||
name: ["", Validators.maxLength(50)],
|
||||
useForEncryption: [false],
|
||||
}),
|
||||
});
|
||||
|
||||
protected credentialOptions?: CredentialCreateOptionsView;
|
||||
protected deviceResponse?: PublicKeyCredential;
|
||||
protected pendingCredential?: PendingWebauthnLoginCredentialView;
|
||||
protected hasPasskeys$?: Observable<boolean>;
|
||||
protected loading$ = this.webauthnService.loading$;
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private dialogRef: DialogRef,
|
||||
private webauthnService: WebauthnLoginService,
|
||||
private webauthnService: WebauthnLoginAdminService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private logService: LogService
|
||||
@@ -112,8 +116,8 @@ export class CreateCredentialDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected async submitCredentialCreation() {
|
||||
this.deviceResponse = await this.webauthnService.createCredential(this.credentialOptions);
|
||||
if (this.deviceResponse === undefined) {
|
||||
this.pendingCredential = await this.webauthnService.createCredential(this.credentialOptions);
|
||||
if (this.pendingCredential === undefined) {
|
||||
this.currentStep = "credentialCreationFailed";
|
||||
return;
|
||||
}
|
||||
@@ -128,16 +132,30 @@ export class CreateCredentialDialogComponent implements OnInit {
|
||||
|
||||
protected async submitCredentialNaming() {
|
||||
this.formGroup.controls.credentialNaming.markAllAsTouched();
|
||||
if (this.formGroup.controls.credentialNaming.invalid) {
|
||||
if (this.formGroup.controls.credentialNaming.controls.name.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
let keySet: PrfKeySet | undefined;
|
||||
if (this.formGroup.value.credentialNaming.useForEncryption) {
|
||||
keySet = await this.webauthnService.createKeySet(this.pendingCredential);
|
||||
|
||||
if (keySet === undefined) {
|
||||
this.formGroup.controls.credentialNaming.controls.useForEncryption?.setErrors({
|
||||
error: {
|
||||
message: this.i18nService.t("useForVaultEncryptionErrorReadingPasskey"),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const name = this.formGroup.value.credentialNaming.name;
|
||||
try {
|
||||
await this.webauthnService.saveCredential(
|
||||
this.credentialOptions,
|
||||
this.deviceResponse,
|
||||
this.formGroup.value.credentialNaming.name
|
||||
this.formGroup.value.credentialNaming.name,
|
||||
this.pendingCredential,
|
||||
keySet
|
||||
);
|
||||
} catch (error) {
|
||||
this.logService?.error(error);
|
||||
|
||||
@@ -10,8 +10,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { Verification } from "@bitwarden/common/types/verification";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { WebauthnLoginService } from "../../../core";
|
||||
import { WebauthnCredentialView } from "../../../core/views/webauth-credential.view";
|
||||
import { WebauthnLoginAdminService } from "../../../core";
|
||||
import { WebauthnLoginCredentialView } from "../../../core/views/webauthn-login-credential.view";
|
||||
|
||||
export interface DeleteCredentialDialogParams {
|
||||
credentialId: string;
|
||||
@@ -27,14 +27,14 @@ export class DeleteCredentialDialogComponent implements OnInit, OnDestroy {
|
||||
protected formGroup = this.formBuilder.group({
|
||||
secret: null as Verification | null,
|
||||
});
|
||||
protected credential?: WebauthnCredentialView;
|
||||
protected credential?: WebauthnLoginCredentialView;
|
||||
protected loading$ = this.webauthnService.loading$;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) private params: DeleteCredentialDialogParams,
|
||||
private formBuilder: FormBuilder,
|
||||
private dialogRef: DialogRef,
|
||||
private webauthnService: WebauthnLoginService,
|
||||
private webauthnService: WebauthnLoginAdminService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private logService: LogService
|
||||
|
||||
@@ -22,12 +22,20 @@
|
||||
<table *ngIf="hasCredentials" class="tw-mb-5">
|
||||
<tr *ngFor="let credential of credentials">
|
||||
<td class="tw-p-2 tw-pl-0 tw-font-semibold">{{ credential.name }}</td>
|
||||
<td class="tw-p-2 tw-pr-10">
|
||||
<ng-container *ngIf="credential.prfSupport">
|
||||
<td class="tw-p-2 tw-pr-10 tw-text-left">
|
||||
<ng-container *ngIf="credential.prfStatus === WebauthnLoginCredentialPrfStatus.Enabled">
|
||||
<i class="bwi bwi-lock-encrypted"></i>
|
||||
{{ "supportsEncryption" | i18n }}
|
||||
<span bitTypography="body1" class="tw-text-muted">{{ "usedForEncryption" | i18n }}</span>
|
||||
</ng-container>
|
||||
<span bitTypography="body1" class="tw-text-muted">
|
||||
<ng-container *ngIf="credential.prfStatus === WebauthnLoginCredentialPrfStatus.Supported">
|
||||
<i class="bwi bwi-lock-encrypted"></i>
|
||||
<span bitTypography="body1" class="tw-text-muted">{{ "encryptionNotEnabled" | i18n }}</span>
|
||||
</ng-container>
|
||||
<span
|
||||
*ngIf="credential.prfStatus === WebauthnLoginCredentialPrfStatus.Unsupported"
|
||||
bitTypography="body1"
|
||||
class="tw-text-muted"
|
||||
>
|
||||
{{ "encryptionNotSupported" | i18n }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -3,8 +3,9 @@ import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { WebauthnLoginService } from "../../core";
|
||||
import { WebauthnCredentialView } from "../../core/views/webauth-credential.view";
|
||||
import { WebauthnLoginAdminService } from "../../core";
|
||||
import { WebauthnLoginCredentialPrfStatus } from "../../core/enums/webauthn-login-credential-prf-status.enum";
|
||||
import { WebauthnLoginCredentialView } from "../../core/views/webauthn-login-credential.view";
|
||||
|
||||
import { openCreateCredentialDialog } from "./create-credential-dialog/create-credential-dialog.component";
|
||||
import { openDeleteCredentialDialogComponent } from "./delete-credential-dialog/delete-credential-dialog.component";
|
||||
@@ -19,13 +20,14 @@ import { openDeleteCredentialDialogComponent } from "./delete-credential-dialog/
|
||||
export class WebauthnLoginSettingsComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected readonly MaxCredentialCount = WebauthnLoginService.MaxCredentialCount;
|
||||
protected readonly MaxCredentialCount = WebauthnLoginAdminService.MaxCredentialCount;
|
||||
protected readonly WebauthnLoginCredentialPrfStatus = WebauthnLoginCredentialPrfStatus;
|
||||
|
||||
protected credentials?: WebauthnCredentialView[];
|
||||
protected credentials?: WebauthnLoginCredentialView[];
|
||||
protected loading = true;
|
||||
|
||||
constructor(
|
||||
private webauthnService: WebauthnLoginService,
|
||||
private webauthnService: WebauthnLoginAdminService,
|
||||
private dialogService: DialogService
|
||||
) {}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import { CheckboxModule } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { UserVerificationModule } from "../../shared/components/user-verification";
|
||||
|
||||
@@ -9,7 +11,7 @@ import { DeleteCredentialDialogComponent } from "./delete-credential-dialog/dele
|
||||
import { WebauthnLoginSettingsComponent } from "./webauthn-login-settings.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, FormsModule, ReactiveFormsModule, UserVerificationModule],
|
||||
imports: [SharedModule, FormsModule, ReactiveFormsModule, UserVerificationModule, CheckboxModule],
|
||||
declarations: [
|
||||
WebauthnLoginSettingsComponent,
|
||||
CreateCredentialDialogComponent,
|
||||
|
||||
Reference in New Issue
Block a user