Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
32af249
PM-13632: Enable sign in with passkeys in the browser extension
abergs Sep 11, 2025
4c77de2
Refactor component + Icon fix
abergs Sep 16, 2025
935efe0
Add tracking links
abergs Sep 16, 2025
9042503
Update app.module.ts
abergs Sep 16, 2025
9860ae1
Remove default Icons on load
abergs Sep 17, 2025
85281b7
Remove login.module.ts
abergs Sep 17, 2025
7df9721
Add env changer to the passkey component
abergs Sep 17, 2025
308ee89
Remove leftover dependencies
abergs Sep 17, 2025
37d3d2e
PRF Unlock
abergs Sep 18, 2025
821a713
Workaround prf type missing
abergs Sep 30, 2025
25586cf
Fix any type
abergs Sep 30, 2025
e7bf0a0
Undo accidental cleanup to keep PR focused
abergs Oct 1, 2025
0a8045d
Undo accidental cleanup to keep PR focused
abergs Oct 1, 2025
b50f845
Cleaned up public interface
abergs Oct 1, 2025
62c4796
Use UserId type
abergs Oct 1, 2025
0437660
Typed UserId and improved isPrfUnlockAvailable
abergs Oct 6, 2025
1c2f7da
Rename key and use zero challenge array
abergs Oct 6, 2025
c612616
logservice
abergs Oct 6, 2025
c121ef3
Cleanup rpId handling
abergs Oct 6, 2025
5259edc
Refactor to separate component + icon
abergs Oct 6, 2025
8092bfc
Moved the prf unlock service impl.
abergs Oct 7, 2025
dd17532
Fix broken test
abergs Oct 7, 2025
07fc6b1
fix tests
abergs Oct 7, 2025
8232f21
Merge branch 'main' into anders/unlock-prf-3
abergs Oct 7, 2025
df2dd00
Use isChromium
abergs Oct 7, 2025
a53b5a8
Update services.module.ts
abergs Oct 7, 2025
f32586c
missing , in locales
abergs Oct 7, 2025
d055532
Update desktop-lock-component.service.ts
abergs Oct 7, 2025
ec48f8c
Fix more desktoptests
abergs Oct 7, 2025
5cd63a4
Expect a single UnlockOption from IdTokenResponse, but multiple from โ€ฆ
abergs Oct 8, 2025
9bf897d
Missing s
abergs Oct 8, 2025
88872fb
remove catches
abergs Oct 21, 2025
6db2221
Use new control flow in unlock-via-prf.component.ts
abergs Oct 21, 2025
3391639
Changed throw behaviour of unlockVaultWithPrf
abergs Oct 21, 2025
18546d4
remove timeout comment
abergs Oct 21, 2025
8772f78
refactired webauthm-prf-unlock.service internally
abergs Oct 21, 2025
d8f7afb
WebAuthnPrfUnlockServiceAbstraction -> WebAuthnPrfUnlockService
abergs Oct 21, 2025
117709f
Fixed any and bad import
abergs Oct 21, 2025
df4caf6
Merge branch 'main' into anders/unlock-prf-3
abergs Oct 21, 2025
d18777b
Fix errors after merge
abergs Oct 21, 2025
b1288d5
Added missing PinServiceAbstraction
abergs Oct 21, 2025
c293b14
Fixed format
abergs Oct 23, 2025
c45c9f9
Removed @Inject()
abergs Oct 23, 2025
cad51ae
Fix broken tests after Inject removal
abergs Oct 23, 2025
7684a5e
Merge branch 'main' into anders/unlock-prf-3
abergs Oct 27, 2025
ef875ad
Return userkey instead of setting it
abergs Oct 28, 2025
b01dbbf
Used input/output signals
abergs Oct 28, 2025
c53cfe3
removed duplicate MessageSender registration
abergs Oct 28, 2025
26a628a
nit: Made import relative
abergs Nov 5, 2025
caafd3d
Disable onPush requirement because it would need refactoring the compโ€ฆ
abergs Nov 5, 2025
377664c
Added feature flag (#17494)
abergs Nov 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/browser/src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"logInWithPasskey": {
"message": "Log in with passkey"
},
"unlockWithPasskey": {
"message": "Unlock with passkey"
},
"useSingleSignOn": {
"message": "Use single sign-on"
},
Expand Down Expand Up @@ -1507,6 +1510,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..."
},
Expand Down Expand Up @@ -3141,6 +3153,12 @@
"error": {
"message": "Error"
},
"prfUnlockFailed": {
"message": "Failed to unlock with passkey. Please try again or use another unlock method."
},
"noPrfCredentialsAvailable": {
"message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first."
},
"decryptionError": {
"message": "Decryption error"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.isFirefox() && !this.platformUtilsService.isSafari();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
BiometricsService,
BiometricsStatus,
BiometricStateService,
WebAuthnPrfUnlockServiceAbstraction,
} from "@bitwarden/key-management";
import { UnlockOptions } from "@bitwarden/key-management-ui";

Expand All @@ -34,6 +35,7 @@ describe("ExtensionLockComponentService", () => {
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let routerService: MockProxy<BrowserRouterService>;
let biometricStateService: MockProxy<BiometricStateService>;
let webAuthnPrfUnlockService: MockProxy<WebAuthnPrfUnlockServiceAbstraction>;

beforeEach(() => {
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
Expand All @@ -43,6 +45,7 @@ describe("ExtensionLockComponentService", () => {
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
routerService = mock<BrowserRouterService>();
biometricStateService = mock<BiometricStateService>();
webAuthnPrfUnlockService = mock<WebAuthnPrfUnlockServiceAbstraction>();

TestBed.configureTestingModule({
providers: [
Expand Down Expand Up @@ -75,6 +78,10 @@ describe("ExtensionLockComponentService", () => {
provide: BiometricStateService,
useValue: biometricStateService,
},
{
provide: WebAuthnPrfUnlockServiceAbstraction,
useValue: webAuthnPrfUnlockService,
},
],
});

Expand Down Expand Up @@ -212,6 +219,9 @@ describe("ExtensionLockComponentService", () => {
enabled: true,
biometricsStatus: BiometricsStatus.Available,
},
prf: {
enabled: false,
},
},
],
[
Expand All @@ -234,6 +244,9 @@ describe("ExtensionLockComponentService", () => {
enabled: true,
biometricsStatus: BiometricsStatus.Available,
},
prf: {
enabled: false,
},
},
],
[
Expand All @@ -256,6 +269,9 @@ describe("ExtensionLockComponentService", () => {
enabled: true,
biometricsStatus: BiometricsStatus.Available,
},
prf: {
enabled: false,
},
},
],
[
Expand All @@ -278,6 +294,9 @@ describe("ExtensionLockComponentService", () => {
enabled: true,
biometricsStatus: BiometricsStatus.Available,
},
prf: {
enabled: false,
},
},
],
[
Expand All @@ -300,6 +319,9 @@ describe("ExtensionLockComponentService", () => {
enabled: false,
biometricsStatus: BiometricsStatus.UnlockNeeded,
},
prf: {
enabled: false,
},
},
],
[
Expand All @@ -322,6 +344,9 @@ describe("ExtensionLockComponentService", () => {
enabled: false,
biometricsStatus: BiometricsStatus.NotEnabledInConnectedDesktopApp,
},
prf: {
enabled: false,
},
},
],
[
Expand All @@ -344,6 +369,9 @@ describe("ExtensionLockComponentService", () => {
enabled: false,
biometricsStatus: BiometricsStatus.HardwareUnavailable,
},
prf: {
enabled: false,
},
},
],
];
Expand Down Expand Up @@ -374,6 +402,10 @@ describe("ExtensionLockComponentService", () => {
// PIN
pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable);

// PRF
webAuthnPrfUnlockService.isPrfUnlockAvailable.mockResolvedValue(false);
webAuthnPrfUnlockService.getPrfUnlockCredentials.mockResolvedValue([]);

const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId));

expect(unlockOptions).toEqual(expectedOutput);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// FIXME (PM-22628): angular imports are forbidden in background
// eslint-disable-next-line no-restricted-imports
import { inject } from "@angular/core";
import { Injectable } from "@angular/core";
import { combineLatest, defer, firstValueFrom, map, Observable } from "rxjs";

import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import {
WebAuthnPrfUnlockServiceAbstraction,
BiometricsService,
BiometricsStatus,
BiometricStateService,
Expand All @@ -20,12 +21,16 @@ import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils";
// eslint-disable-next-line no-restricted-imports
import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service";

@Injectable()
export class ExtensionLockComponentService implements LockComponentService {
private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction);
private readonly biometricsService = inject(BiometricsService);
private readonly pinService = inject(PinServiceAbstraction);
private readonly routerService = inject(BrowserRouterService);
private readonly biometricStateService = inject(BiometricStateService);
constructor(
private readonly userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private readonly biometricsService: BiometricsService,
private readonly pinService: PinServiceAbstraction,
private readonly biometricStateService: BiometricStateService,
private readonly routerService: BrowserRouterService,
private readonly webAuthnPrfUnlockService: WebAuthnPrfUnlockServiceAbstraction,
) {}

getPreviousUrl(): string | null {
return this.routerService.getPreviousUrl() ?? null;
Expand Down Expand Up @@ -81,8 +86,16 @@ export class ExtensionLockComponentService implements LockComponentService {
}),
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
defer(() => this.pinService.isPinDecryptionAvailable(userId)),
defer(async () => {
try {
const available = await this.webAuthnPrfUnlockService.isPrfUnlockAvailable(userId);
return { available };
} catch {
return { available: false };
}
}),
]).pipe(
map(([biometricsStatus, userDecryptionOptions, pinDecryptionAvailable]) => {
map(([biometricsStatus, userDecryptionOptions, pinDecryptionAvailable, prfUnlockInfo]) => {
const unlockOpts: UnlockOptions = {
masterPassword: {
enabled: userDecryptionOptions?.hasMasterPassword,
Expand All @@ -94,6 +107,9 @@ export class ExtensionLockComponentService implements LockComponentService {
enabled: biometricsStatus === BiometricsStatus.Available,
biometricsStatus: biometricsStatus,
},
prf: {
enabled: prfUnlockInfo.available,
},
};
return unlockOpts;
}),
Expand Down
25 changes: 25 additions & 0 deletions apps/browser/src/popup/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
Expand All @@ -23,6 +24,7 @@ import {
UserLockIcon,
VaultIcon,
LockIcon,
TwoFactorAuthSecurityKeyIcon,
} from "@bitwarden/assets/svg";
import {
LoginComponent,
Expand Down Expand Up @@ -401,6 +403,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)],
Expand Down
42 changes: 27 additions & 15 deletions apps/browser/src/popup/services/services.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ import {
LoginEmailService,
SsoUrlService,
LogoutService,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { BrowserRouterService } from "@bitwarden/browser/platform/popup/services/browser-router.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
Expand All @@ -49,6 +51,7 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction";
import { AuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/auth-request-answering.service";
import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state";
import {
Expand Down Expand Up @@ -134,9 +137,12 @@ import {
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import {
BiometricsService,
BiometricStateService,
DefaultKeyService,
KdfConfigService,
KeyService,
WebAuthnPrfUnlockService,
WebAuthnPrfUnlockServiceAbstraction,
} from "@bitwarden/key-management";
import { LockComponentService } from "@bitwarden/key-management-ui";
import { DerivedStateProvider, GlobalStateProvider, StateProvider } from "@bitwarden/state";
Expand Down Expand Up @@ -529,15 +535,6 @@ const safeProviders: SafeProvider[] = [
useFactory: () => new Subject<Message<Record<string, unknown>>>(),
deps: [],
}),
safeProvider({
provide: MessageSender,
useFactory: (subject: Subject<Message<Record<string, unknown>>>, logService: LogService) =>
MessageSender.combine(
new SubjectMessageSender(subject), // For sending messages in the same context
new ChromeMessageSender(logService), // For sending messages to different contexts
),
deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService],
}),
safeProvider({
provide: DISK_BACKUP_LOCAL_STORAGE,
useFactory: (diskStorage: AbstractStorageService & ObservableStorageService) =>
Expand All @@ -561,7 +558,14 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: LockComponentService,
useClass: ExtensionLockComponentService,
deps: [],
deps: [
UserDecryptionOptionsServiceAbstraction,
BiometricsService,
PinServiceAbstraction,
BiometricStateService,
BrowserRouterService,
WebAuthnPrfUnlockServiceAbstraction,
],
}),
// TODO: PM-18182 - Refactor component services into lazy loaded modules
safeProvider({
Expand All @@ -584,11 +588,6 @@ const safeProviders: SafeProvider[] = [
PlatformUtilsService,
],
}),
safeProvider({
provide: ActionsService,
useClass: BrowserActionsService,
deps: [LogService, PlatformUtilsService],
}),
safeProvider({
provide: SystemNotificationsService,
useFactory: (platformUtilsService: PlatformUtilsService) => {
Expand All @@ -605,6 +604,19 @@ const safeProviders: SafeProvider[] = [
useClass: Fido2UserVerificationService,
deps: [PasswordRepromptService, UserVerificationService, DialogService],
}),
safeProvider({
provide: WebAuthnPrfUnlockServiceAbstraction,
useClass: WebAuthnPrfUnlockService,
deps: [
WebAuthnLoginPrfKeyServiceAbstraction,
KeyService,
UserDecryptionOptionsServiceAbstraction,
EncryptService,
EnvironmentService,
WINDOW,
LogService,
],
}),
safeProvider({
provide: AnimationControlService,
useClass: DefaultAnimationControlService,
Expand Down
Loading
Loading