1
0
mirror of https://github.com/bitwarden/browser synced 2026-03-01 02:51:24 +00:00
Files
browser/libs/key-management-ui/src/lock/components/master-password-lock/master-password-lock.component.ts
Anders Åberg 903026b574 PM-2035: PRF Unlock (web + extension) (#16662)
* PM-13632: Enable sign in with passkeys in the browser extension

* Refactor component + Icon fix

This commit refactors the login-via-webauthn commit as per @JaredSnider-Bitwarden suggestions. It also fixes an existing issue where Icons are not displayed properly on the web vault.

Remove old one.

Rename the file

Working refactor

Removed the icon from the component

Fixed icons not showing. Changed layout to be 'embedded'

* Add tracking links

* Update app.module.ts

* Remove default Icons on load

* Remove login.module.ts

* Add env changer to the passkey component

* Remove leftover dependencies

* PRF Unlock

Cleanup and testes

* Workaround prf type missing

* Fix any type

* Undo accidental cleanup to keep PR focused

* Undo accidental cleanup to keep PR focused

* Cleaned up public interface

* Use UserId type

* Typed UserId and improved isPrfUnlockAvailable

* Rename key and use zero challenge array

* logservice

* Cleanup rpId handling

* Refactor to separate component + icon

* Moved the prf unlock service impl.

* Fix broken test

* fix tests

* Use isChromium

* Update services.module.ts

* missing , in locales

* Update desktop-lock-component.service.ts

* Fix more desktoptests

* Expect a single UnlockOption from IdTokenResponse, but multiple from sync

* Missing s

* remove catches

* Use new control flow in unlock-via-prf.component.ts

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Changed throw behaviour of unlockVaultWithPrf

* remove timeout comment

* refactired webauthm-prf-unlock.service internally

* WebAuthnPrfUnlockServiceAbstraction -> WebAuthnPrfUnlockService

* Fixed any and bad import

* Fix errors after merge

* Added missing PinServiceAbstraction

* Fixed format

* Removed @Inject()

* Fix broken tests after Inject removal

* Return userkey instead of setting it

* Used input/output signals

* removed duplicate MessageSender registration

* nit: Made import relative

* Disable onPush requirement because it would need refactoring the component

* Added feature flag (#17494)

* Fixed ById from main

* Import feature flag from file

* Add missing test providers for MasterPasswordLockComponent

Add WebAuthnPrfUnlockService and DialogService mocks to fix test failures
caused by UnlockViaPrfComponent dependencies.

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
2026-01-26 10:53:20 +01:00

154 lines
5.1 KiB
TypeScript

import {
Component,
computed,
inject,
input,
model,
OnDestroy,
OnInit,
output,
} from "@angular/core";
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
import { firstValueFrom, Subject, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ClientType } from "@bitwarden/client-type";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserKey } from "@bitwarden/common/types/key";
import {
AsyncActionsModule,
ButtonModule,
FormFieldModule,
IconButtonModule,
ToastService,
} from "@bitwarden/components";
import { BiometricsStatus } from "@bitwarden/key-management";
import { LogService } from "@bitwarden/logging";
import { CommandDefinition, MessageListener } from "@bitwarden/messaging";
import { UserId } from "@bitwarden/user-core";
import {
UnlockOption,
UnlockOptions,
UnlockOptionValue,
} from "../../services/lock-component.service";
import { UnlockViaPrfComponent } from "../unlock-via-prf.component";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "bit-master-password-lock",
templateUrl: "master-password-lock.component.html",
imports: [
JslibModule,
ReactiveFormsModule,
ButtonModule,
FormFieldModule,
AsyncActionsModule,
IconButtonModule,
UnlockViaPrfComponent,
],
})
export class MasterPasswordLockComponent implements OnInit, OnDestroy {
private readonly accountService = inject(AccountService);
private readonly masterPasswordUnlockService = inject(MasterPasswordUnlockService);
private readonly i18nService = inject(I18nService);
private readonly toastService = inject(ToastService);
private readonly logService = inject(LogService);
private readonly platformUtilsService = inject(PlatformUtilsService);
private readonly messageListener = inject(MessageListener);
UnlockOption = UnlockOption;
readonly activeUnlockOption = model.required<UnlockOptionValue>();
readonly unlockOptions = input.required<UnlockOptions>();
readonly biometricUnlockBtnText = input.required<string>();
readonly showPinSwap = computed(() => this.unlockOptions().pin.enabled ?? false);
readonly biometricsAvailable = computed(() => this.unlockOptions().biometrics.enabled ?? false);
readonly showBiometricsSwap = computed(() => {
const status = this.unlockOptions().biometrics.biometricsStatus;
return (
status !== BiometricsStatus.PlatformUnsupported &&
status !== BiometricsStatus.NotEnabledLocally
);
});
successfulUnlock = output<{ userKey: UserKey; masterPassword: string }>();
prfUnlockSuccess = output<UserKey>();
logOut = output<void>();
protected showPassword = false;
private destroy$ = new Subject<void>();
formGroup = new FormGroup({
masterPassword: new FormControl("", {
validators: [Validators.required],
updateOn: "submit",
}),
});
async ngOnInit(): Promise<void> {
if (this.platformUtilsService.getClientType() === ClientType.Desktop) {
this.messageListener
.messages$(new CommandDefinition("windowHidden"))
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.showPassword = false;
});
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
submit = async () => {
this.formGroup.markAllAsTouched();
const masterPassword = this.formGroup.controls.masterPassword.value;
if (this.formGroup.invalid || !masterPassword) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordRequired"),
});
return;
}
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
await this.unlockViaMasterPassword(masterPassword, activeUserId);
};
private async unlockViaMasterPassword(
masterPassword: string,
activeUserId: UserId,
): Promise<void> {
try {
const userKey = await this.masterPasswordUnlockService.unlockWithMasterPassword(
masterPassword,
activeUserId,
);
this.successfulUnlock.emit({ userKey, masterPassword });
} catch (error) {
this.logService.error(
"[MasterPasswordLockComponent] Failed to unlock via master password",
error,
);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("invalidMasterPassword"),
});
}
}
onPrfUnlockSuccess(userKey: UserKey): void {
this.prfUnlockSuccess.emit(userKey);
}
}