1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00

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()
This commit is contained in:
Anders Åberg
2025-10-06 15:25:51 +02:00
committed by GitHub
parent 525a6003bc
commit 6cbdecef43
11 changed files with 162 additions and 102 deletions

View File

@@ -1548,6 +1548,15 @@
"readSecurityKey": { "readSecurityKey": {
"message": "Read security key" "message": "Read security key"
}, },
"readingPasskeyLoading": {
"message": "Reading passkey..."
},
"passkeyAuthenticationFailed": {
"message": "Passkey authentication failed"
},
"useADifferentLogInMethod": {
"message": "Use a different log in method"
},
"awaitingSecurityKeyInteraction": { "awaitingSecurityKeyInteraction": {
"message": "Awaiting security key interaction..." "message": "Awaiting security key interaction..."
}, },

View File

@@ -68,4 +68,18 @@ export class ExtensionLoginComponentService
showBackButton(showBackButton: boolean): void { showBackButton(showBackButton: boolean): void {
this.extensionAnonLayoutWrapperDataService.setAnonLayoutWrapperData({ showBackButton }); 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();
}
} }

View File

@@ -12,6 +12,7 @@ import {
tdeDecryptionRequiredGuard, tdeDecryptionRequiredGuard,
unauthGuardFn, unauthGuardFn,
} from "@bitwarden/angular/auth/guards"; } 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 { 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 { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component";
import { import {
@@ -22,6 +23,7 @@ import {
UserLockIcon, UserLockIcon,
VaultIcon, VaultIcon,
LockIcon, LockIcon,
TwoFactorAuthSecurityKeyIcon,
DeactivatedOrg, DeactivatedOrg,
} from "@bitwarden/assets/svg"; } from "@bitwarden/assets/svg";
import { 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", path: "sso",
canActivate: [unauthGuardFn(unauthRouteOverrides)], canActivate: [unauthGuardFn(unauthRouteOverrides)],

View File

@@ -1,54 +0,0 @@
<div
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8"
>
<div>
<img class="logo logo-themed" alt="Bitwarden" />
<h3 bitTypography="h3" class="tw-my-8 tw-mb-3 tw-text-center">
{{ "readingPasskeyLoading" | i18n }}
</h3>
<div
class="tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
>
<div class="tw-flex tw-flex-col tw-items-center">
<ng-container *ngIf="currentState === 'assert'">
<div class="tw-size-24 tw-content-center tw-my-10">
<bit-icon [icon]="Icons.TwoFactorAuthSecurityKeyIcon"></bit-icon>
</div>
<p bitTypography="body1">{{ "readingPasskeyLoadingInfo" | i18n }}</p>
<button
type="button"
bitButton
block
[loading]="true"
buttonType="primary"
class="tw-mb-4"
>
{{ "loading" | i18n }}
</button>
</ng-container>
<ng-container *ngIf="currentState === 'assertFailed'">
<div class="tw-size-24 tw-content-center tw-my-10">
<bit-icon [icon]="Icons.TwoFactorAuthSecurityKeyFailedIcon"></bit-icon>
</div>
<p bitTypography="body1">{{ "readingPasskeyLoadingInfo" | i18n }}</p>
<button
type="button"
bitButton
block
buttonType="primary"
class="tw-mb-4"
(click)="retry()"
>
{{ "tryAgain" | i18n }}
</button>
</ng-container>
</div>
<p bitTypography="body1" class="tw-mb-0">
{{ "troubleLoggingIn" | i18n }}<br />
<a bitLink routerLink="/login">{{ "useADifferentLogInMethod" | i18n }}</a>
</p>
</div>
</div>
</div>

View File

@@ -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,
};
}

View File

@@ -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 {}

View File

@@ -10,6 +10,7 @@ import {
unauthGuardFn, unauthGuardFn,
activeAuthGuard, activeAuthGuard,
} from "@bitwarden/angular/auth/guards"; } 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 { 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 { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component";
import { import {
@@ -17,6 +18,7 @@ import {
RegistrationUserAddIcon, RegistrationUserAddIcon,
TwoFactorTimeoutIcon, TwoFactorTimeoutIcon,
TwoFactorAuthEmailIcon, TwoFactorAuthEmailIcon,
TwoFactorAuthSecurityKeyIcon,
UserLockIcon, UserLockIcon,
VaultIcon, VaultIcon,
SsoKeyIcon, SsoKeyIcon,
@@ -49,7 +51,6 @@ import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/
import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component"; import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component";
import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component"; import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
import { deepLinkGuard } from "./auth/guards/deep-link/deep-link.guard"; 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 { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component";
import { RecoverDeleteComponent } from "./auth/recover-delete.component"; import { RecoverDeleteComponent } from "./auth/recover-delete.component";
import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component"; import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component";
@@ -106,11 +107,6 @@ const routes: Routes = [
children: [], // Children lets us have an empty component. children: [], // Children lets us have an empty component.
canActivate: [redirectGuard()], // Redirects either to vault, login, or lock page. 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: "verify-email", component: VerifyEmailTokenComponent },
{ {
path: "accept-organization", path: "accept-organization",
@@ -140,6 +136,28 @@ const routes: Routes = [
path: "", path: "",
component: AnonLayoutWrapperComponent, component: AnonLayoutWrapperComponent,
children: [ 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", path: "signup",
canActivate: [unauthGuardFn()], canActivate: [unauthGuardFn()],

View File

@@ -1,7 +1,6 @@
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { AuthModule } from "./auth"; import { AuthModule } from "./auth";
import { LoginModule } from "./auth/login/login.module";
import { TrialInitiationModule } from "./billing/trial-initiation/trial-initiation.module"; import { TrialInitiationModule } from "./billing/trial-initiation/trial-initiation.module";
import { HeaderModule } from "./layouts/header/header.module"; import { HeaderModule } from "./layouts/header/header.module";
import { SharedModule } from "./shared"; import { SharedModule } from "./shared";
@@ -21,7 +20,6 @@ import "./shared/locales";
TrialInitiationModule, TrialInitiationModule,
VaultFilterModule, VaultFilterModule,
OrganizationBadgeModule, OrganizationBadgeModule,
LoginModule,
AuthModule, AuthModule,
AccessComponent, AccessComponent,
], ],
@@ -31,7 +29,6 @@ import "./shared/locales";
TrialInitiationModule, TrialInitiationModule,
VaultFilterModule, VaultFilterModule,
OrganizationBadgeModule, OrganizationBadgeModule,
LoginModule,
AccessComponent, AccessComponent,
], ],
bootstrap: [], bootstrap: [],

View File

@@ -1248,6 +1248,9 @@
"readingPasskeyLoadingInfo": { "readingPasskeyLoadingInfo": {
"message": "Keep this window open and follow prompts from your browser." "message": "Keep this window open and follow prompts from your browser."
}, },
"passkeyAuthenticationFailed": {
"message": "Passkey authentication failed. Please try again."
},
"useADifferentLogInMethod": { "useADifferentLogInMethod": {
"message": "Use a different log in method" "message": "Use a different log in method"
}, },

View File

@@ -0,0 +1,20 @@
<div class="tw-flex tw-flex-col tw-items-center">
<ng-container *ngIf="currentState === 'assert'">
<p bitTypography="body1" class="tw-text-center">{{ "readingPasskeyLoading" | i18n }}</p>
<button type="button" bitButton block [loading]="true" buttonType="primary" class="tw-mb-4">
{{ "loading" | i18n }}
</button>
</ng-container>
<ng-container *ngIf="currentState === 'assertFailed'">
<p bitTypography="body1" class="tw-text-center">{{ "passkeyAuthenticationFailed" | i18n }}</p>
<button type="button" bitButton block buttonType="primary" class="tw-mb-4" (click)="retry()">
{{ "tryAgain" | i18n }}
</button>
</ng-container>
<p bitTypography="body1" class="tw-mb-0 tw-text-center">
{{ "troubleLoggingIn" | i18n }}<br />
<a bitLink routerLink="/login">{{ "useADifferentLogInMethod" | i18n }}</a>
</p>
</div>

View File

@@ -1,27 +1,69 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { Directive, OnInit } from "@angular/core"; import { CommonModule } from "@angular/common";
import { Router } from "@angular/router"; import { Component, OnInit } from "@angular/core";
import { Router, RouterModule } from "@angular/router";
import { firstValueFrom } from "rxjs"; 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. // 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 // eslint-disable-next-line no-restricted-imports
import { LoginSuccessHandlerService } from "@bitwarden/auth/common"; import { LoginSuccessHandlerService } from "@bitwarden/auth/common";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; 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 { 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 { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import {
AnonLayoutWrapperDataService,
ButtonModule,
IconModule,
LinkModule,
TypographyModule,
} from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management"; import { KeyService } from "@bitwarden/key-management";
export type State = "assert" | "assertFailed"; export type State = "assert" | "assertFailed";
@Component({
@Directive() selector: "app-login-via-webauthn",
export class BaseLoginViaWebAuthnComponent implements OnInit { 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 currentState: State = "assert";
protected successRoute = "/vault"; protected readonly Icons = {
TwoFactorAuthSecurityKeyIcon,
TwoFactorAuthSecurityKeyFailedIcon,
};
private readonly successRoutes: Record<ClientType, string> = {
[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( constructor(
private webAuthnLoginService: WebAuthnLoginServiceAbstraction, private webAuthnLoginService: WebAuthnLoginServiceAbstraction,
@@ -31,6 +73,8 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
private i18nService: I18nService, private i18nService: I18nService,
private loginSuccessHandlerService: LoginSuccessHandlerService, private loginSuccessHandlerService: LoginSuccessHandlerService,
private keyService: KeyService, private keyService: KeyService,
private platformUtilsService: PlatformUtilsService,
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@@ -41,6 +85,8 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
protected retry() { protected retry() {
this.currentState = "assert"; 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. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.authenticate(); this.authenticate();
@@ -54,6 +100,7 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
} catch (error) { } catch (error) {
this.validationService.showError(error); this.validationService.showError(error);
this.currentState = "assertFailed"; this.currentState = "assertFailed";
this.setFailureIcon();
return; return;
} }
try { try {
@@ -64,6 +111,7 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
this.i18nService.t("twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn"), this.i18nService.t("twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn"),
); );
this.currentState = "assertFailed"; this.currentState = "assertFailed";
this.setFailureIcon();
return; return;
} }
@@ -80,6 +128,19 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
} }
this.logService.error(error); this.logService.error(error);
this.currentState = "assertFailed"; 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,
});
}
} }