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, + }); + } }