From 6cbdecef43065798e6855d85ce72a9df1416c4bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 6 Oct 2025 15:25:51 +0200 Subject: [PATCH] PM-13632: Enable sign in with passkeys in the browser extension for chromium browsers (#16385) * 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 * use .isChromium() --- apps/browser/src/_locales/en/messages.json | 9 +++ .../extension-login-component.service.ts | 14 ++++ apps/browser/src/popup/app-routing.module.ts | 25 +++++++ .../login-via-webauthn.component.html | 54 -------------- .../login-via-webauthn.component.ts | 19 ----- apps/web/src/app/auth/login/login.module.ts | 14 ---- apps/web/src/app/oss-routing.module.ts | 30 ++++++-- apps/web/src/app/oss.module.ts | 3 - apps/web/src/locales/en/messages.json | 3 + .../login-via-webauthn.component.html | 20 +++++ .../login-via-webauthn.component.ts} | 73 +++++++++++++++++-- 11 files changed, 162 insertions(+), 102 deletions(-) delete mode 100644 apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.html delete mode 100644 apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.ts delete mode 100644 apps/web/src/app/auth/login/login.module.ts create mode 100644 libs/angular/src/auth/login-via-webauthn/login-via-webauthn.component.html rename libs/angular/src/auth/{components/base-login-via-webauthn.component.ts => login-via-webauthn/login-via-webauthn.component.ts} (62%) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index df47d357746..d91a33c6796 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1548,6 +1548,15 @@ "readSecurityKey": { "message": "Read security key" }, + "readingPasskeyLoading": { + "message": "Reading passkey..." + }, + "passkeyAuthenticationFailed": { + "message": "Passkey authentication failed" + }, + "useADifferentLogInMethod": { + "message": "Use a different log in method" + }, "awaitingSecurityKeyInteraction": { "message": "Awaiting security key interaction..." }, diff --git a/apps/browser/src/auth/popup/login/extension-login-component.service.ts b/apps/browser/src/auth/popup/login/extension-login-component.service.ts index 37d74616391..621c7d74876 100644 --- a/apps/browser/src/auth/popup/login/extension-login-component.service.ts +++ b/apps/browser/src/auth/popup/login/extension-login-component.service.ts @@ -68,4 +68,18 @@ export class ExtensionLoginComponentService showBackButton(showBackButton: boolean): void { this.extensionAnonLayoutWrapperDataService.setAnonLayoutWrapperData({ showBackButton }); } + + /** + * Enable passkey login support for chromium-based browsers only. + * Neither Firefox nor safari support overriding the relying party ID in an extension. + * + * https://github.com/w3c/webextensions/issues/238 + * + * Tracking links: + * https://bugzilla.mozilla.org/show_bug.cgi?id=1956484 + * https://developer.apple.com/forums/thread/774351 + */ + isLoginWithPasskeySupported(): boolean { + return this.platformUtilsService.isChromium(); + } } diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index b69d7b73672..17a812f451c 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -12,6 +12,7 @@ import { tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; +import { LoginViaWebAuthnComponent } from "@bitwarden/angular/auth/login-via-webauthn/login-via-webauthn.component"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password"; import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component"; import { @@ -22,6 +23,7 @@ import { UserLockIcon, VaultIcon, LockIcon, + TwoFactorAuthSecurityKeyIcon, DeactivatedOrg, } from "@bitwarden/assets/svg"; import { @@ -403,6 +405,29 @@ const routes: Routes = [ }, ], }, + { + path: "login-with-passkey", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { + pageIcon: TwoFactorAuthSecurityKeyIcon, + pageTitle: { + key: "logInWithPasskey", + }, + pageSubtitle: { + key: "readingPasskeyLoadingInfo", + }, + elevation: 1, + showBackButton: true, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [ + { path: "", component: LoginViaWebAuthnComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, { path: "sso", canActivate: [unauthGuardFn(unauthRouteOverrides)], diff --git a/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.html b/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.html deleted file mode 100644 index 94dfac42976..00000000000 --- a/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.html +++ /dev/null @@ -1,54 +0,0 @@ -
-
- -

- {{ "readingPasskeyLoading" | i18n }} -

- -
-
- -
- -
-

{{ "readingPasskeyLoadingInfo" | i18n }}

- -
- - -
- -
-

{{ "readingPasskeyLoadingInfo" | i18n }}

- -
-
-

- {{ "troubleLoggingIn" | i18n }}
- {{ "useADifferentLogInMethod" | i18n }} -

-
-
-
diff --git a/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.ts b/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.ts deleted file mode 100644 index 695e935b919..00000000000 --- a/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Component } from "@angular/core"; - -import { BaseLoginViaWebAuthnComponent } from "@bitwarden/angular/auth/components/base-login-via-webauthn.component"; -import { - TwoFactorAuthSecurityKeyIcon, - TwoFactorAuthSecurityKeyFailedIcon, -} from "@bitwarden/assets/svg"; - -@Component({ - selector: "app-login-via-webauthn", - templateUrl: "login-via-webauthn.component.html", - standalone: false, -}) -export class LoginViaWebAuthnComponent extends BaseLoginViaWebAuthnComponent { - protected readonly Icons = { - TwoFactorAuthSecurityKeyIcon, - TwoFactorAuthSecurityKeyFailedIcon, - }; -} diff --git a/apps/web/src/app/auth/login/login.module.ts b/apps/web/src/app/auth/login/login.module.ts deleted file mode 100644 index 9a99c84f727..00000000000 --- a/apps/web/src/app/auth/login/login.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NgModule } from "@angular/core"; - -import { CheckboxModule } from "@bitwarden/components"; - -import { SharedModule } from "../../../app/shared"; - -import { LoginViaWebAuthnComponent } from "./login-via-webauthn/login-via-webauthn.component"; - -@NgModule({ - imports: [SharedModule, CheckboxModule], - declarations: [LoginViaWebAuthnComponent], - exports: [LoginViaWebAuthnComponent], -}) -export class LoginModule {} diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 9ac628752b6..7ffe69b7ee6 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -10,6 +10,7 @@ import { unauthGuardFn, activeAuthGuard, } from "@bitwarden/angular/auth/guards"; +import { LoginViaWebAuthnComponent } from "@bitwarden/angular/auth/login-via-webauthn/login-via-webauthn.component"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password"; import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component"; import { @@ -17,6 +18,7 @@ import { RegistrationUserAddIcon, TwoFactorTimeoutIcon, TwoFactorAuthEmailIcon, + TwoFactorAuthSecurityKeyIcon, UserLockIcon, VaultIcon, SsoKeyIcon, @@ -49,7 +51,6 @@ import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/ import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component"; import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component"; import { deepLinkGuard } from "./auth/guards/deep-link/deep-link.guard"; -import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component"; import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component"; import { RecoverDeleteComponent } from "./auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component"; @@ -106,11 +107,6 @@ const routes: Routes = [ children: [], // Children lets us have an empty component. canActivate: [redirectGuard()], // Redirects either to vault, login, or lock page. }, - { - path: "login-with-passkey", - component: LoginViaWebAuthnComponent, - data: { titleId: "logInWithPasskey" } satisfies RouteDataProperties, - }, { path: "verify-email", component: VerifyEmailTokenComponent }, { path: "accept-organization", @@ -140,6 +136,28 @@ const routes: Routes = [ path: "", component: AnonLayoutWrapperComponent, children: [ + { + path: "login-with-passkey", + canActivate: [unauthGuardFn()], + data: { + pageIcon: TwoFactorAuthSecurityKeyIcon, + titleId: "logInWithPasskey", + pageTitle: { + key: "logInWithPasskey", + }, + pageSubtitle: { + key: "readingPasskeyLoadingInfo", + }, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + children: [ + { path: "", component: LoginViaWebAuthnComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, { path: "signup", canActivate: [unauthGuardFn()], diff --git a/apps/web/src/app/oss.module.ts b/apps/web/src/app/oss.module.ts index 4e04910246f..ce1b45a9e47 100644 --- a/apps/web/src/app/oss.module.ts +++ b/apps/web/src/app/oss.module.ts @@ -1,7 +1,6 @@ import { NgModule } from "@angular/core"; import { AuthModule } from "./auth"; -import { LoginModule } from "./auth/login/login.module"; import { TrialInitiationModule } from "./billing/trial-initiation/trial-initiation.module"; import { HeaderModule } from "./layouts/header/header.module"; import { SharedModule } from "./shared"; @@ -21,7 +20,6 @@ import "./shared/locales"; TrialInitiationModule, VaultFilterModule, OrganizationBadgeModule, - LoginModule, AuthModule, AccessComponent, ], @@ -31,7 +29,6 @@ import "./shared/locales"; TrialInitiationModule, VaultFilterModule, OrganizationBadgeModule, - LoginModule, AccessComponent, ], bootstrap: [], diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 5ed393c0295..7f08d3f02d1 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1248,6 +1248,9 @@ "readingPasskeyLoadingInfo": { "message": "Keep this window open and follow prompts from your browser." }, + "passkeyAuthenticationFailed": { + "message": "Passkey authentication failed. Please try again." + }, "useADifferentLogInMethod": { "message": "Use a different log in method" }, diff --git a/libs/angular/src/auth/login-via-webauthn/login-via-webauthn.component.html b/libs/angular/src/auth/login-via-webauthn/login-via-webauthn.component.html new file mode 100644 index 00000000000..1fe0f18ceb7 --- /dev/null +++ b/libs/angular/src/auth/login-via-webauthn/login-via-webauthn.component.html @@ -0,0 +1,20 @@ +
+ +

{{ "readingPasskeyLoading" | i18n }}

+ +
+ + +

{{ "passkeyAuthenticationFailed" | i18n }}

+ +
+ +

+ {{ "troubleLoggingIn" | i18n }}
+ {{ "useADifferentLogInMethod" | i18n }} +

+
diff --git a/libs/angular/src/auth/components/base-login-via-webauthn.component.ts b/libs/angular/src/auth/login-via-webauthn/login-via-webauthn.component.ts similarity index 62% rename from libs/angular/src/auth/components/base-login-via-webauthn.component.ts rename to libs/angular/src/auth/login-via-webauthn/login-via-webauthn.component.ts index 53e29d4d940..f795b66d916 100644 --- a/libs/angular/src/auth/components/base-login-via-webauthn.component.ts +++ b/libs/angular/src/auth/login-via-webauthn/login-via-webauthn.component.ts @@ -1,27 +1,69 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Directive, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { Router, RouterModule } from "@angular/router"; import { firstValueFrom } from "rxjs"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + TwoFactorAuthSecurityKeyIcon, + TwoFactorAuthSecurityKeyFailedIcon, +} from "@bitwarden/assets/svg"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { LoginSuccessHandlerService } from "@bitwarden/auth/common"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view"; +import { ClientType } from "@bitwarden/common/enums"; 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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { + AnonLayoutWrapperDataService, + ButtonModule, + IconModule, + LinkModule, + TypographyModule, +} from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; export type State = "assert" | "assertFailed"; - -@Directive() -export class BaseLoginViaWebAuthnComponent implements OnInit { +@Component({ + selector: "app-login-via-webauthn", + templateUrl: "login-via-webauthn.component.html", + standalone: true, + imports: [ + CommonModule, + RouterModule, + JslibModule, + ButtonModule, + IconModule, + LinkModule, + TypographyModule, + ], +}) +export class LoginViaWebAuthnComponent implements OnInit { protected currentState: State = "assert"; - protected successRoute = "/vault"; + protected readonly Icons = { + TwoFactorAuthSecurityKeyIcon, + TwoFactorAuthSecurityKeyFailedIcon, + }; + + private readonly successRoutes: Record = { + [ClientType.Web]: "/vault", + [ClientType.Browser]: "/tabs/vault", + [ClientType.Desktop]: "/vault", + [ClientType.Cli]: "/vault", + }; + + protected get successRoute(): string { + const clientType = this.platformUtilsService.getClientType(); + return this.successRoutes[clientType] || "/vault"; + } constructor( private webAuthnLoginService: WebAuthnLoginServiceAbstraction, @@ -31,6 +73,8 @@ export class BaseLoginViaWebAuthnComponent implements OnInit { private i18nService: I18nService, private loginSuccessHandlerService: LoginSuccessHandlerService, private keyService: KeyService, + private platformUtilsService: PlatformUtilsService, + private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, ) {} ngOnInit(): void { @@ -41,6 +85,8 @@ export class BaseLoginViaWebAuthnComponent implements OnInit { protected retry() { this.currentState = "assert"; + // Reset to default icon on retry + this.setDefaultIcon(); // 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.authenticate(); @@ -54,6 +100,7 @@ export class BaseLoginViaWebAuthnComponent implements OnInit { } catch (error) { this.validationService.showError(error); this.currentState = "assertFailed"; + this.setFailureIcon(); return; } try { @@ -64,6 +111,7 @@ export class BaseLoginViaWebAuthnComponent implements OnInit { this.i18nService.t("twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn"), ); this.currentState = "assertFailed"; + this.setFailureIcon(); return; } @@ -80,6 +128,19 @@ export class BaseLoginViaWebAuthnComponent implements OnInit { } this.logService.error(error); this.currentState = "assertFailed"; + this.setFailureIcon(); } } + + private setDefaultIcon(): void { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageIcon: this.Icons.TwoFactorAuthSecurityKeyIcon, + }); + } + + private setFailureIcon(): void { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageIcon: this.Icons.TwoFactorAuthSecurityKeyFailedIcon, + }); + } }