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 @@
-
-
-
![Bitwarden]()
-
- {{ "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,
+ });
+ }
}