From 095ce7ec30061e49dafc9fbff8581f3f9571e57d Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Wed, 4 Sep 2024 13:50:48 -0400 Subject: [PATCH] [PM-7956] Update passkey pop-out to new UI (#10796) * rename existing fido2 components to use v1 designation * use fido2 message type value constants in components * add v2 fido2 components * add search to login UX of fido2 v2 component * add new item button in top nav of fido2 v2 component * get and pass activeUserId to cipher key decription methods * cleanup / PR suggestions --- apps/browser/src/_locales/en/messages.json | 10 +- .../src/auth/popup/lock.component.html | 2 +- .../fido2/content/messaging/message.ts | 4 +- .../fido2/fido2-cipher-row-v1.component.html | 36 ++ .../fido2/fido2-cipher-row-v1.component.ts | 39 ++ .../fido2/fido2-cipher-row.component.html | 57 +-- .../popup/fido2/fido2-cipher-row.component.ts | 25 +- .../fido2-use-browser-link-v1.component.html | 52 ++ .../fido2-use-browser-link-v1.component.ts | 113 +++++ .../fido2/fido2-use-browser-link.component.ts | 11 +- .../popup/fido2/fido2-v1.component.html | 142 ++++++ .../popup/fido2/fido2-v1.component.ts | 443 ++++++++++++++++++ .../autofill/popup/fido2/fido2.component.html | 254 +++++----- .../autofill/popup/fido2/fido2.component.ts | 233 +++++---- apps/browser/src/popup/app-routing.module.ts | 8 +- apps/browser/src/popup/app.module.ts | 12 +- apps/browser/src/popup/scss/pages.scss | 2 +- 17 files changed, 1159 insertions(+), 284 deletions(-) create mode 100644 apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html create mode 100644 apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts create mode 100644 apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html create mode 100644 apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts create mode 100644 apps/browser/src/autofill/popup/fido2/fido2-v1.component.html create mode 100644 apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 89b605ab63..3aa1ac097c 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3477,7 +3477,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3489,6 +3489,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3498,9 +3501,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, diff --git a/apps/browser/src/auth/popup/lock.component.html b/apps/browser/src/auth/popup/lock.component.html index ccc743d86d..fb1b09de49 100644 --- a/apps/browser/src/auth/popup/lock.component.html +++ b/apps/browser/src/auth/popup/lock.component.html @@ -94,7 +94,7 @@ {{ "awaitDesktop" | i18n }}

- + diff --git a/apps/browser/src/autofill/fido2/content/messaging/message.ts b/apps/browser/src/autofill/fido2/content/messaging/message.ts index d42c10a5d8..5815be9eb6 100644 --- a/apps/browser/src/autofill/fido2/content/messaging/message.ts +++ b/apps/browser/src/autofill/fido2/content/messaging/message.ts @@ -18,7 +18,7 @@ export enum MessageType { } /** - * The params provided by the page-script are created in an insecure environemnt and + * The params provided by the page-script are created in an insecure environment and * should not be trusted. This type is used to ensure that the content-script does not * trust the `origin` or `sameOriginWithAncestors` params. */ @@ -38,7 +38,7 @@ export type CredentialCreationResponse = { }; /** - * The params provided by the page-script are created in an insecure environemnt and + * The params provided by the page-script are created in an insecure environment and * should not be trusted. This type is used to ensure that the content-script does not * trust the `origin` or `sameOriginWithAncestors` params. */ diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html new file mode 100644 index 0000000000..852fd4a0e8 --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html @@ -0,0 +1,36 @@ +
+
+ +
+
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts new file mode 100644 index 0000000000..d9d492bdcc --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts @@ -0,0 +1,39 @@ +import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core"; + +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +@Component({ + selector: "app-fido2-cipher-row-v1", + templateUrl: "fido2-cipher-row-v1.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class Fido2CipherRowV1Component { + @Output() onSelected = new EventEmitter(); + @Input() cipher: CipherView; + @Input() last: boolean; + @Input() title: string; + @Input() isSearching: boolean; + @Input() isSelected: boolean; + + protected selectCipher(c: CipherView) { + this.onSelected.emit(c); + } + + /** + * Returns a subname for the cipher. + * If this has a FIDO2 credential, and the cipher.name is different from the FIDO2 credential's rpId, return the rpId. + * @param c Cipher + * @returns + */ + protected getSubName(c: CipherView): string | null { + const fido2Credentials = c.login?.fido2Credentials; + + if (!fido2Credentials || fido2Credentials.length === 0) { + return null; + } + + const [fido2Credential] = fido2Credentials; + + return c.name !== fido2Credential.rpId ? fido2Credential.rpId : null; + } +} diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html index 852fd4a0e8..0328a91bff 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html @@ -1,36 +1,21 @@ -
-
- -
-
+ + + diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts index 25d623b169..91bcd6494e 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts @@ -1,19 +1,40 @@ +import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + BadgeModule, + ButtonModule, + IconButtonModule, + ItemModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, +} from "@bitwarden/components"; @Component({ selector: "app-fido2-cipher-row", templateUrl: "fido2-cipher-row.component.html", changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + BadgeModule, + ButtonModule, + CommonModule, + IconButtonModule, + ItemModule, + JslibModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + ], }) export class Fido2CipherRowComponent { @Output() onSelected = new EventEmitter(); @Input() cipher: CipherView; @Input() last: boolean; @Input() title: string; - @Input() isSearching: boolean; - @Input() isSelected: boolean; protected selectCipher(c: CipherView) { this.onSelected.emit(c); diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html new file mode 100644 index 0000000000..9f6c0aca50 --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html @@ -0,0 +1,52 @@ + + + + +
+ +
+
+ +
+
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts new file mode 100644 index 0000000000..cf79dfc652 --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts @@ -0,0 +1,113 @@ +import { animate, state, style, transition, trigger } from "@angular/animations"; +import { ConnectedPosition } from "@angular/cdk/overlay"; +import { Component } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; + +import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data"; +import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service"; + +@Component({ + selector: "app-fido2-use-browser-link-v1", + templateUrl: "fido2-use-browser-link-v1.component.html", + animations: [ + trigger("transformPanel", [ + state( + "void", + style({ + opacity: 0, + }), + ), + transition( + "void => open", + animate( + "100ms linear", + style({ + opacity: 1, + }), + ), + ), + transition("* => void", animate("100ms linear", style({ opacity: 0 }))), + ]), + ], +}) +export class Fido2UseBrowserLinkV1Component { + showOverlay = false; + isOpen = false; + overlayPosition: ConnectedPosition[] = [ + { + originX: "start", + originY: "bottom", + overlayX: "start", + overlayY: "top", + offsetY: 5, + }, + ]; + + protected fido2PopoutSessionData$ = fido2PopoutSessionData$(); + + constructor( + private domainSettingsService: DomainSettingsService, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + ) {} + + toggle() { + this.isOpen = !this.isOpen; + } + + close() { + this.isOpen = false; + } + + /** + * Aborts the current FIDO2 session and fallsback to the browser. + * @param excludeDomain - Identifies if the domain should be excluded from future FIDO2 prompts. + */ + protected async abort(excludeDomain = true) { + this.close(); + const sessionData = await firstValueFrom(this.fido2PopoutSessionData$); + + if (!excludeDomain) { + this.abortSession(sessionData.sessionId); + return; + } + // Show overlay to prevent the user from interacting with the page. + this.showOverlay = true; + await this.handleDomainExclusion(sessionData.senderUrl); + // Give the user a chance to see the toast before closing the popout. + await Utils.delay(2000); + this.abortSession(sessionData.sessionId); + } + + /** + * Excludes the domain from future FIDO2 prompts. + * @param uri - The domain uri to exclude from future FIDO2 prompts. + */ + private async handleDomainExclusion(uri: string) { + const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); + + const validDomain = Utils.getHostname(uri); + const savedDomains: NeverDomains = { + ...existingDomains, + }; + savedDomains[validDomain] = null; + + await this.domainSettingsService.setNeverDomains(savedDomains); + + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("domainAddedToExcludedDomains", validDomain), + ); + } + + private abortSession(sessionId: string) { + BrowserFido2UserInterfaceSession.abortPopout(sessionId, true); + } +} diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts index d9a7c7c9cb..86f13d29c7 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts @@ -1,8 +1,11 @@ import { animate, state, style, transition, trigger } from "@angular/animations"; -import { ConnectedPosition } from "@angular/cdk/overlay"; +import { A11yModule } from "@angular/cdk/a11y"; +import { ConnectedPosition, CdkOverlayOrigin, CdkConnectedOverlay } from "@angular/cdk/overlay"; +import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -15,6 +18,8 @@ import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-f @Component({ selector: "app-fido2-use-browser-link", templateUrl: "fido2-use-browser-link.component.html", + standalone: true, + imports: [A11yModule, CdkConnectedOverlay, CdkOverlayOrigin, CommonModule, JslibModule], animations: [ trigger("transformPanel", [ state( @@ -90,11 +95,11 @@ export class Fido2UseBrowserLinkComponent { * @param uri - The domain uri to exclude from future FIDO2 prompts. */ private async handleDomainExclusion(uri: string) { - const exisitingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); + const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); const validDomain = Utils.getHostname(uri); const savedDomains: NeverDomains = { - ...exisitingDomains, + ...existingDomains, }; savedDomains[validDomain] = null; diff --git a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html new file mode 100644 index 0000000000..8a052fbc5b --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html @@ -0,0 +1,142 @@ + +
+
+
+ + + + + + +
+ + +
+ +
+
+
+ + + +
+

+ {{ subtitleText | i18n }} +

+ + +
+
+ +
+
+ +
+ +
+
+ + +
+ +
+
+
+
+ +
+

{{ "passkeyAlreadyExists" | i18n }}

+
+
+ +
+
+ +
+
+ +
+

{{ "noPasskeysFoundForThisApplication" | i18n }}

+
+ +
+
+ + +
+
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts new file mode 100644 index 0000000000..d6026a8c7a --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts @@ -0,0 +1,443 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { + BehaviorSubject, + combineLatest, + concatMap, + filter, + firstValueFrom, + map, + Observable, + Subject, + take, + takeUntil, +} from "rxjs"; + +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; +import { DialogService } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service"; +import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window"; +import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service"; +import { + BrowserFido2Message, + BrowserFido2UserInterfaceSession, + BrowserFido2MessageTypes, +} from "../../fido2/services/browser-fido2-user-interface.service"; + +interface ViewData { + message: BrowserFido2Message; + fallbackSupported: boolean; +} + +@Component({ + selector: "app-fido2-v1", + templateUrl: "fido2-v1.component.html", + styleUrls: [], +}) +export class Fido2V1Component implements OnInit, OnDestroy { + private destroy$ = new Subject(); + private hasSearched = false; + + protected cipher: CipherView; + protected searchTypeSearch = false; + protected searchPending = false; + protected searchText: string; + protected url: string; + protected hostname: string; + protected data$: Observable; + protected sessionId?: string; + protected senderTabId?: string; + protected ciphers?: CipherView[] = []; + protected displayedCiphers?: CipherView[] = []; + protected loading = false; + protected subtitleText: string; + protected credentialText: string; + protected BrowserFido2MessageTypes = BrowserFido2MessageTypes; + + private message$ = new BehaviorSubject(null); + + constructor( + private router: Router, + private activatedRoute: ActivatedRoute, + private cipherService: CipherService, + private platformUtilsService: PlatformUtilsService, + private domainSettingsService: DomainSettingsService, + private searchService: SearchService, + private logService: LogService, + private dialogService: DialogService, + private browserMessagingApi: ZonedMessageListenerService, + private passwordRepromptService: PasswordRepromptService, + private fido2UserVerificationService: Fido2UserVerificationService, + private accountService: AccountService, + ) {} + + ngOnInit() { + this.searchTypeSearch = !this.platformUtilsService.isSafari(); + + const queryParams$ = this.activatedRoute.queryParamMap.pipe( + take(1), + map((queryParamMap) => ({ + sessionId: queryParamMap.get("sessionId"), + senderTabId: queryParamMap.get("senderTabId"), + senderUrl: queryParamMap.get("senderUrl"), + })), + ); + + combineLatest([ + queryParams$, + this.browserMessagingApi.messageListener$() as Observable, + ]) + .pipe( + concatMap(async ([queryParams, message]) => { + this.sessionId = queryParams.sessionId; + this.senderTabId = queryParams.senderTabId; + this.url = queryParams.senderUrl; + // For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session. + if ( + message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest && + message.sessionId !== queryParams.sessionId + ) { + this.abort(false); + return; + } + + // Ignore messages that don't belong to the current session. + if (message.sessionId !== queryParams.sessionId) { + return; + } + + if (message.type === BrowserFido2MessageTypes.AbortRequest) { + this.abort(false); + return; + } + + return message; + }), + filter((message) => !!message), + takeUntil(this.destroy$), + ) + .subscribe((message) => { + this.message$.next(message); + }); + + this.data$ = this.message$.pipe( + filter((message) => message != undefined), + concatMap(async (message) => { + switch (message.type) { + case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: { + const equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(this.url), + ); + + this.ciphers = (await this.cipherService.getAllDecrypted()).filter( + (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, + ); + this.displayedCiphers = this.ciphers.filter( + (cipher) => + cipher.login.matchesUri(this.url, equivalentDomains) && + this.hasNoOtherPasskeys(cipher, message.userHandle), + ); + + if (this.displayedCiphers.length > 0) { + this.selectedPasskey(this.displayedCiphers[0]); + } + break; + } + + case BrowserFido2MessageTypes.PickCredentialRequest: { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + this.ciphers = await Promise.all( + message.cipherIds.map(async (cipherId) => { + const cipher = await this.cipherService.get(cipherId); + return cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + }), + ); + this.displayedCiphers = [...this.ciphers]; + if (this.displayedCiphers.length > 0) { + this.selectedPasskey(this.displayedCiphers[0]); + } + break; + } + + case BrowserFido2MessageTypes.InformExcludedCredentialRequest: { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + this.ciphers = await Promise.all( + message.existingCipherIds.map(async (cipherId) => { + const cipher = await this.cipherService.get(cipherId); + return cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + }), + ); + this.displayedCiphers = [...this.ciphers]; + + if (this.displayedCiphers.length > 0) { + this.selectedPasskey(this.displayedCiphers[0]); + } + break; + } + } + + this.subtitleText = + this.displayedCiphers.length > 0 + ? this.getCredentialSubTitleText(message.type) + : "noMatchingPasskeyLogin"; + + this.credentialText = this.getCredentialButtonText(message.type); + return { + message, + fallbackSupported: "fallbackSupported" in message && message.fallbackSupported, + }; + }), + takeUntil(this.destroy$), + ); + + queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => { + this.send({ + sessionId: queryParams.sessionId, + type: BrowserFido2MessageTypes.ConnectResponse, + }); + }); + } + + protected async submit() { + const data = this.message$.value; + if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) { + // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. + // PM-4577 - https://github.com/bitwarden/clients/pull/8746 + const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); + + this.send({ + sessionId: this.sessionId, + cipherId: this.cipher.id, + type: BrowserFido2MessageTypes.PickCredentialResponse, + userVerified, + }); + } else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { + if (this.cipher.login.hasFido2Credentials) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "overwritePasskey" }, + content: { key: "overwritePasskeyAlert" }, + type: "info", + }); + + if (!confirmed) { + return false; + } + } + + // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. + // PM-4577 - https://github.com/bitwarden/clients/pull/8746 + const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); + + this.send({ + sessionId: this.sessionId, + cipherId: this.cipher.id, + type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, + userVerified, + }); + } + + this.loading = true; + } + + protected async saveNewLogin() { + const data = this.message$.value; + if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { + const name = data.credentialName || data.rpId; + // TODO: Revert to check for user verification once user verification for passkeys is approved for production. + // PM-4577 - https://github.com/bitwarden/clients/pull/8746 + await this.createNewCipher(name, data.userName); + + // We are bypassing user verification pending approval. + this.send({ + sessionId: this.sessionId, + cipherId: this.cipher?.id, + type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, + userVerified: data.userVerification, + }); + } + + this.loading = true; + } + + getCredentialSubTitleText(messageType: string): string { + return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest + ? "chooseCipherForPasskeySave" + : "logInWithPasskeyQuestion"; + } + + getCredentialButtonText(messageType: string): string { + return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest + ? "savePasskey" + : "confirm"; + } + + selectedPasskey(item: CipherView) { + this.cipher = item; + } + + viewPasskey() { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["/view-cipher"], { + queryParams: { + cipherId: this.cipher.id, + uilocation: "popout", + senderTabId: this.senderTabId, + sessionId: this.sessionId, + singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, + }, + }); + } + + addCipher() { + const data = this.message$.value; + + if (data?.type !== BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { + return; + } + + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["/add-cipher"], { + queryParams: { + name: data.credentialName || data.rpId, + uri: this.url, + type: CipherType.Login.toString(), + uilocation: "popout", + username: data.userName, + senderTabId: this.senderTabId, + sessionId: this.sessionId, + userVerification: data.userVerification, + singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, + }, + }); + } + + protected async search() { + this.hasSearched = await this.searchService.isSearchable(this.searchText); + this.searchPending = true; + if (this.hasSearched) { + this.displayedCiphers = await this.searchService.searchCiphers( + this.searchText, + null, + this.ciphers, + ); + } else { + const equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(this.url), + ); + this.displayedCiphers = this.ciphers.filter((cipher) => + cipher.login.matchesUri(this.url, equivalentDomains), + ); + } + this.searchPending = false; + this.selectedPasskey(this.displayedCiphers[0]); + } + + abort(fallback: boolean) { + this.unload(fallback); + window.close(); + } + + unload(fallback = false) { + this.send({ + sessionId: this.sessionId, + type: BrowserFido2MessageTypes.AbortResponse, + fallbackRequested: fallback, + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + private buildCipher(name: string, username: string) { + this.cipher = new CipherView(); + this.cipher.name = name; + + this.cipher.type = CipherType.Login; + this.cipher.login = new LoginView(); + this.cipher.login.username = username; + this.cipher.login.uris = [new LoginUriView()]; + this.cipher.login.uris[0].uri = this.url; + this.cipher.card = new CardView(); + this.cipher.identity = new IdentityView(); + this.cipher.secureNote = new SecureNoteView(); + this.cipher.secureNote.type = SecureNoteType.Generic; + this.cipher.reprompt = CipherRepromptType.None; + } + + private async createNewCipher(name: string, username: string) { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + this.buildCipher(name, username); + const cipher = await this.cipherService.encrypt(this.cipher, activeUserId); + try { + await this.cipherService.createWithServer(cipher); + this.cipher.id = cipher.id; + } catch (e) { + this.logService.error(e); + } + } + + // TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production. + private async handleUserVerification( + userVerificationRequested: boolean, + cipher: CipherView, + ): Promise { + const masterPasswordRepromptRequired = cipher && cipher.reprompt !== 0; + + if (masterPasswordRepromptRequired) { + return await this.passwordRepromptService.showPasswordPrompt(); + } + + return userVerificationRequested; + } + + private send(msg: BrowserFido2Message) { + BrowserFido2UserInterfaceSession.sendMessage({ + sessionId: this.sessionId, + ...msg, + }); + } + + /** + * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle + * @param userHandle + */ + private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { + if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) { + return true; + } + + return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle); + } +} diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.html b/apps/browser/src/autofill/popup/fido2/fido2.component.html index 9036d6d991..00cd55d31b 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.html +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.html @@ -1,136 +1,134 @@ - -
-
-
- - - + diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.ts b/apps/browser/src/autofill/popup/fido2/fido2.component.ts index 8bd667c17f..c389e9ad5b 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.ts @@ -1,4 +1,6 @@ +import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { BehaviorSubject, @@ -13,13 +15,14 @@ import { takeUntil, } from "rxjs"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; +import { SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -27,17 +30,39 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view" import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; -import { DialogService } from "@bitwarden/components"; +import { + ButtonModule, + DialogService, + Icons, + ItemModule, + NoItemsModule, + SearchModule, + SectionComponent, + SectionHeaderComponent, +} from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service"; +import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window"; import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service"; import { BrowserFido2Message, BrowserFido2UserInterfaceSession, + BrowserFido2MessageTypes, } from "../../fido2/services/browser-fido2-user-interface.service"; +import { Fido2CipherRowComponent } from "./fido2-cipher-row.component"; +import { Fido2UseBrowserLinkComponent } from "./fido2-use-browser-link.component"; + +const PasskeyActions = { + Register: "register", + Authenticate: "authenticate", +} as const; + +type PasskeyActionValue = (typeof PasskeyActions)[keyof typeof PasskeyActions]; + interface ViewData { message: BrowserFido2Message; fallbackSupported: boolean; @@ -46,28 +71,45 @@ interface ViewData { @Component({ selector: "app-fido2", templateUrl: "fido2.component.html", - styleUrls: [], + standalone: true, + imports: [ + ButtonModule, + CommonModule, + Fido2CipherRowComponent, + Fido2UseBrowserLinkComponent, + FormsModule, + ItemModule, + JslibModule, + NoItemsModule, + PopupHeaderComponent, + PopupPageComponent, + SearchModule, + SectionComponent, + SectionHeaderComponent, + ], }) export class Fido2Component implements OnInit, OnDestroy { private destroy$ = new Subject(); - private hasSearched = false; - - protected cipher: CipherView; - protected searchTypeSearch = false; - protected searchPending = false; - protected searchText: string; - protected url: string; - protected hostname: string; - protected data$: Observable; - protected sessionId?: string; - protected senderTabId?: string; - protected ciphers?: CipherView[] = []; - protected displayedCiphers?: CipherView[] = []; - protected loading = false; - protected subtitleText: string; - protected credentialText: string; - private message$ = new BehaviorSubject(null); + private hasSearched = false; + protected BrowserFido2MessageTypes = BrowserFido2MessageTypes; + protected cipher: CipherView; + protected ciphers?: CipherView[] = []; + protected data$: Observable; + protected displayedCiphers?: CipherView[] = []; + protected equivalentDomains: Set; + protected equivalentDomainsURL: string; + protected hostname: string; + protected loading = false; + protected noResultsIcon = Icons.NoResults; + protected passkeyAction: PasskeyActionValue = PasskeyActions.Register; + protected PasskeyActions = PasskeyActions; + protected searchText: string; + protected searchTypeSearch = false; + protected senderTabId?: string; + protected sessionId?: string; + protected showNewPasskeyButton: boolean = false; + protected url: string; constructor( private router: Router, @@ -80,8 +122,8 @@ export class Fido2Component implements OnInit, OnDestroy { private dialogService: DialogService, private browserMessagingApi: ZonedMessageListenerService, private passwordRepromptService: PasswordRepromptService, - private fido2UserVerificationService: Fido2UserVerificationService, private accountService: AccountService, + private fido2UserVerificationService: Fido2UserVerificationService, ) {} ngOnInit() { @@ -107,7 +149,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.url = queryParams.senderUrl; // For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session. if ( - message.type === "NewSessionCreatedRequest" && + message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest && message.sessionId !== queryParams.sessionId ) { this.abort(false); @@ -119,7 +161,7 @@ export class Fido2Component implements OnInit, OnDestroy { return; } - if (message.type === "AbortRequest") { + if (message.type === BrowserFido2MessageTypes.AbortRequest) { this.abort(false); return; } @@ -137,7 +179,7 @@ export class Fido2Component implements OnInit, OnDestroy { filter((message) => message != undefined), concatMap(async (message) => { switch (message.type) { - case "ConfirmNewCredentialRequest": { + case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: { const equivalentDomains = await firstValueFrom( this.domainSettingsService.getUrlEquivalentDomains(this.url), ); @@ -145,19 +187,22 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers = (await this.cipherService.getAllDecrypted()).filter( (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, ); + this.displayedCiphers = this.ciphers.filter( (cipher) => cipher.login.matchesUri(this.url, equivalentDomains) && - this.hasNoOtherPasskeys(cipher, message.userHandle), + this.cipherHasNoOtherPasskeys(cipher, message.userHandle), ); - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } + this.passkeyAction = PasskeyActions.Register; + + // @TODO fix new cipher creation for other fido2 registration message types and remove `showNewPasskeyButton` from the template + this.showNewPasskeyButton = true; + break; } - case "PickCredentialRequest": { + case BrowserFido2MessageTypes.PickCredentialRequest: { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); @@ -170,14 +215,15 @@ export class Fido2Component implements OnInit, OnDestroy { ); }), ); + this.displayedCiphers = [...this.ciphers]; - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } + + this.passkeyAction = PasskeyActions.Authenticate; + break; } - case "InformExcludedCredentialRequest": { + case BrowserFido2MessageTypes.InformExcludedCredentialRequest: { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); @@ -190,40 +236,42 @@ export class Fido2Component implements OnInit, OnDestroy { ); }), ); + this.displayedCiphers = [...this.ciphers]; - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } + this.passkeyAction = PasskeyActions.Register; + + break; + } + + case BrowserFido2MessageTypes.InformCredentialNotFoundRequest: { + this.passkeyAction = PasskeyActions.Authenticate; + break; } } - this.subtitleText = - this.displayedCiphers.length > 0 - ? this.getCredentialSubTitleText(message.type) - : "noMatchingPasskeyLogin"; - - this.credentialText = this.getCredentialButtonText(message.type); return { message, fallbackSupported: "fallbackSupported" in message && message.fallbackSupported, }; }), + takeUntil(this.destroy$), ); queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => { this.send({ sessionId: queryParams.sessionId, - type: "ConnectResponse", + type: BrowserFido2MessageTypes.ConnectResponse, }); }); } protected async submit() { const data = this.message$.value; - if (data?.type === "PickCredentialRequest") { + + if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) { // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. // PM-4577 - https://github.com/bitwarden/clients/pull/8746 const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); @@ -231,10 +279,10 @@ export class Fido2Component implements OnInit, OnDestroy { this.send({ sessionId: this.sessionId, cipherId: this.cipher.id, - type: "PickCredentialResponse", + type: BrowserFido2MessageTypes.PickCredentialResponse, userVerified, }); - } else if (data?.type === "ConfirmNewCredentialRequest") { + } else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { if (this.cipher.login.hasFido2Credentials) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "overwritePasskey" }, @@ -254,7 +302,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.send({ sessionId: this.sessionId, cipherId: this.cipher.id, - type: "ConfirmNewCredentialResponse", + type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, userVerified, }); } @@ -264,7 +312,8 @@ export class Fido2Component implements OnInit, OnDestroy { protected async saveNewLogin() { const data = this.message$.value; - if (data?.type === "ConfirmNewCredentialRequest") { + + if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { const name = data.credentialName || data.rpId; // TODO: Revert to check for user verification once user verification for passkeys is approved for production. // PM-4577 - https://github.com/bitwarden/clients/pull/8746 @@ -274,7 +323,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.send({ sessionId: this.sessionId, cipherId: this.cipher?.id, - type: "ConfirmNewCredentialResponse", + type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, userVerified: data.userVerification, }); } @@ -282,59 +331,47 @@ export class Fido2Component implements OnInit, OnDestroy { this.loading = true; } - getCredentialSubTitleText(messageType: string): string { - return messageType == "ConfirmNewCredentialRequest" ? "choosePasskey" : "logInWithPasskey"; - } - - getCredentialButtonText(messageType: string): string { - return messageType == "ConfirmNewCredentialRequest" ? "savePasskey" : "confirm"; - } - - selectedPasskey(item: CipherView) { + async handleCipherItemSelect(item: CipherView) { this.cipher = item; + + await this.submit(); } - viewPasskey() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/view-cipher"], { - queryParams: { - cipherId: this.cipher.id, - uilocation: "popout", - senderTabId: this.senderTabId, - sessionId: this.sessionId, - singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, - }, - }); - } - - addCipher() { + async addCipher() { const data = this.message$.value; - if (data?.type !== "ConfirmNewCredentialRequest") { - return; + if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { + await this.router.navigate(["/add-cipher"], { + queryParams: { + type: CipherType.Login.toString(), + name: data.credentialName || data.rpId, + uri: this.url, + uilocation: "popout", + username: data.userName, + senderTabId: this.senderTabId, + sessionId: this.sessionId, + userVerification: data.userVerification, + singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, + }, + }); } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/add-cipher"], { - queryParams: { - name: data.credentialName || data.rpId, - uri: this.url, - type: CipherType.Login.toString(), - uilocation: "popout", - username: data.userName, - senderTabId: this.senderTabId, - sessionId: this.sessionId, - userVerification: data.userVerification, - singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, - }, - }); + return; + } + + async getEquivalentDomains() { + if (this.equivalentDomainsURL !== this.url) { + this.equivalentDomainsURL = this.url; + this.equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(this.url), + ); + } + + return this.equivalentDomains; } protected async search() { this.hasSearched = await this.searchService.isSearchable(this.searchText); - this.searchPending = true; if (this.hasSearched) { this.displayedCiphers = await this.searchService.searchCiphers( this.searchText, @@ -342,15 +379,11 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers, ); } else { - const equivalentDomains = await firstValueFrom( - this.domainSettingsService.getUrlEquivalentDomains(this.url), - ); + const equivalentDomains = await this.getEquivalentDomains(); this.displayedCiphers = this.ciphers.filter((cipher) => cipher.login.matchesUri(this.url, equivalentDomains), ); } - this.searchPending = false; - this.selectedPasskey(this.displayedCiphers[0]); } abort(fallback: boolean) { @@ -361,7 +394,7 @@ export class Fido2Component implements OnInit, OnDestroy { unload(fallback = false) { this.send({ sessionId: this.sessionId, - type: "AbortResponse", + type: BrowserFido2MessageTypes.AbortResponse, fallbackRequested: fallback, }); } @@ -427,13 +460,11 @@ export class Fido2Component implements OnInit, OnDestroy { * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle * @param userHandle */ - private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { + private cipherHasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) { return true; } - return cipher.login.fido2Credentials.some((passkey) => { - passkey.userHandle === userHandle; - }); + return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle); } } diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 82e673a9e5..aa8955035d 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -41,6 +41,7 @@ import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component" import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; +import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component"; import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; @@ -127,12 +128,11 @@ const routes: Routes = [ canActivate: [unauthGuardFn(unauthRouteOverrides)], data: { state: "home" }, }, - { + ...extensionRefreshSwap(Fido2V1Component, Fido2Component, { path: "fido2", - component: Fido2Component, canActivate: [fido2AuthGuard], data: { state: "fido2" }, - }, + }), { path: "login", component: LoginComponent, @@ -304,7 +304,6 @@ const routes: Routes = [ }, ...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, { path: "notifications", - component: NotificationsSettingsV1Component, canActivate: [authGuard], data: { state: "notifications" }, }), @@ -338,7 +337,6 @@ const routes: Routes = [ }, ...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, { path: "excluded-domains", - component: ExcludedDomainsV1Component, canActivate: [authGuard], data: { state: "excluded-domains" }, }), diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 56ddd3c6ba..f8d3c69105 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -35,8 +35,11 @@ import { SsoComponent } from "../auth/popup/sso.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; +import { Fido2CipherRowV1Component } from "../autofill/popup/fido2/fido2-cipher-row-v1.component"; import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component"; +import { Fido2UseBrowserLinkV1Component } from "../autofill/popup/fido2/fido2-use-browser-link-v1.component"; import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component"; +import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component"; import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; @@ -112,6 +115,9 @@ import "../platform/popup/locales"; ServicesModule, DialogModule, ExcludedDomainsComponent, + Fido2CipherRowComponent, + Fido2Component, + Fido2UseBrowserLinkComponent, FilePopoutCalloutComponent, AvatarModule, AccountComponent, @@ -140,8 +146,8 @@ import "../platform/popup/locales"; CurrentTabComponent, EnvironmentComponent, ExcludedDomainsV1Component, - Fido2CipherRowComponent, - Fido2UseBrowserLinkComponent, + Fido2CipherRowV1Component, + Fido2UseBrowserLinkV1Component, FolderAddEditComponent, FoldersComponent, VaultFilterComponent, @@ -180,7 +186,7 @@ import "../platform/popup/locales"; ViewCustomFieldsComponent, RemovePasswordComponent, VaultSelectComponent, - Fido2Component, + Fido2V1Component, AutofillV1Component, EnvironmentSelectorComponent, ], diff --git a/apps/browser/src/popup/scss/pages.scss b/apps/browser/src/popup/scss/pages.scss index 3ae3647299..bf8f03e7d0 100644 --- a/apps/browser/src/popup/scss/pages.scss +++ b/apps/browser/src/popup/scss/pages.scss @@ -217,7 +217,7 @@ app-vault-attachments { } } -app-fido2 { +app-fido2-v1 { .auth-wrapper { display: flex; flex-direction: column;