From 9f0ae4b79933b06fb24d638e9ebeb72a4e67fdb1 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 12 Jan 2026 13:52:46 -0600 Subject: [PATCH] Throw if silent assert request requires showing UI --- .../services/desktop-autofill.service.ts | 12 +++++++++--- .../desktop-fido2-user-interface.service.ts | 19 ++++++++++++------- ...fido2-authenticator.service.abstraction.ts | 8 +++++++- ...ido2-user-interface.service.abstraction.ts | 9 +++++++++ .../fido2/fido2-authenticator.service.ts | 1 + 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 0590579c2b6..2c1fdce1a09 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -307,7 +307,7 @@ export class DesktopAutofillService implements OnDestroy { } const response = await this.fido2AuthenticatorService.getAssertion( - this.convertAssertionRequest(request, true), + this.convertAssertionRequest(request, { assumeUserPresence: true, isSilent: true }), { windowXy: request.windowXy, handle: clientHandle }, controller, request.context, @@ -453,15 +453,20 @@ export class DesktopAutofillService implements OnDestroy { /** * * @param request - * @param assumeUserPresence For WithoutUserInterface requests, we assume the user is present + * @param options For WithoutUserInterface requests, we assume user presence and throw errors if we cannot fulfill the request silently. * @returns */ private convertAssertionRequest( request: | autofill.PasskeyAssertionRequest | autofill.PasskeyAssertionWithoutUserInterfaceRequest, - assumeUserPresence: boolean = false, + options?: { + assumeUserPresence?: boolean, + isSilent?: boolean, + } + ): Fido2AuthenticatorGetAssertionParams { + const { assumeUserPresence, isSilent } = options ?? {}; let allowedCredentials; if ("credentialId" in request) { allowedCredentials = [ @@ -486,6 +491,7 @@ export class DesktopAutofillService implements OnDestroy { request.userVerification === "required" || request.userVerification === "preferred", fallbackSupported: false, assumeUserPresence, + isSilent, }; } diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index b636a677a1b..f0571b0c956 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -18,6 +18,7 @@ import { Fido2UserInterfaceSession, NewCredentialParams, PickCredentialParams, +UserInteractionRequired, } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -130,18 +131,20 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi userVerification, assumeUserPresence, masterPasswordRepromptRequired, + isSilent, }: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { this.logService.debug("pickCredential desktop function", { cipherIds, userVerification, assumeUserPresence, masterPasswordRepromptRequired, + isSilent, }); try { // Check if we can return the credential without user interaction await this.accountService.setShowHeader(false); - if (assumeUserPresence && cipherIds.length === 1 && !masterPasswordRepromptRequired) { + if (cipherIds.length === 1 && !masterPasswordRepromptRequired) { const selectedCipherId = cipherIds[0]; if (userVerification) { // retrieve the cipher @@ -166,7 +169,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi this.logService.debug("Failed to prompt for user verification without showing UI", e) } } - else { + else if (assumeUserPresence) { this.logService.warning( "shortcut - Assuming user presence and returning cipherId", cipherIds[0], @@ -175,11 +178,13 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi } } - this.logService.debug("Could not shortcut, showing UI"); - - // TODO: We need to pass context from the original request whether this - // should be a silent request or not. Then, we can fail here if it's - // supposed to be silent. + if (isSilent) { + this.logService.info("Could not fulfill request silently, aborting request"); + throw new UserInteractionRequired() + } + else { + this.logService.debug("Could not shortcut, showing UI"); + } // make the cipherIds available to the UI. this.availableCipherIdsSubject.next(cipherIds); diff --git a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts index 1a3ad39e990..fcef9bb2400 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts @@ -153,8 +153,14 @@ export interface Fido2AuthenticatorGetAssertionParams { /** Forwarded to user interface */ fallbackSupported: boolean; - // Bypass the UI and assume that the user has already interacted with the authenticator + /** Bypass the UI and assume that the user has already interacted with the authenticator */ assumeUserPresence?: boolean; + + /** Signals whether an error should be thrown if an assertion cannot be obtained without showing Bitwarden UI. + * + * Note that OS user verification prompts are allowed in silent requests. + */ + isSilent?: boolean; } export interface Fido2AuthenticatorGetAssertionResult { diff --git a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts index 21581cfbe40..f3e8d97fcfe 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -50,6 +50,12 @@ export interface PickCredentialParams { * Identifies whether a cipher requires a master password reprompt when getting a credential. */ masterPasswordRepromptRequired?: boolean; + + /** Signals whether an error should be thrown if an assertion cannot be obtained without showing Bitwarden UI. + * + * Note that OS user verification prompts are allowed in silent requests. + */ + isSilent?: boolean; } /** @@ -121,3 +127,6 @@ export abstract class Fido2UserInterfaceSession { */ abstract close(): void; } + +/** Thrown when user interaction is required during a request for a silent assertion. */ +export class UserInteractionRequired extends Error {} \ No newline at end of file diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts index 8140e2dfff6..a3ba6489134 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts @@ -291,6 +291,7 @@ export class Fido2AuthenticatorService< userVerification: params.requireUserVerification, assumeUserPresence: params.assumeUserPresence, masterPasswordRepromptRequired, + isSilent: params.isSilent, }); }