1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-23 03:22:50 +02:00

[PM-11419] Fix issues encountered with inline menu passkeys (#10892)

* [PM-11419] Login items do not display after adding passkey

* [PM-11419] Login items do not display after adding passkey

* [PM-11419] Incorporating fixes for deleting a cipher from the inline menu as well as authenticating using passkeys

* [PM-11419] Fixing an issue where master password reprompt is ignored for a set passkey cipher

* [PM-11419] Fixing an issue where saving a passkey does not trigger a clearing of cached cipher values

* [PM-11419] Refactoring implementation

* [PM-11419] Ensuring that passkeys must be enabled in order for ciphers to appear

* [PM-11419] Adding an abort event from the active request manager

* [PM-11419] Adding an abort event from the active request manager

* [PM-11419] Working through jest tests within implementation

* [PM-11419] Fixing jest tests within Fido2ClientService and Fido2AuthenticatorService

* [PM-11419] Adding jest tests for added logic within OverlayBackground

* [PM-11419] Adding jest tests for added logic within OverlayBackground

* [PM-11419] Reworking how we handle assuming user presence when master password reprompt is required

* [PM-11419] Reworking how we handle assuming user presence when master password reprompt is required

* [PM-11419] Reworking how we handle assuming user presence when master password reprompt is required

* [PM-11419] Refactoring implementation

* [PM-11419] Incorporating suggestion for reporting failed passkey authentication from the inline menu

* [PM-11419] Reworking positioning of the abort controller that informs the background script of an error

* [PM-11419] Scoping down the behavior surrounding master password reprompt a bit more tightly

* [PM-11419] Reworking how we handle reacting to active fido2 requests to avoid ambiguity

* [PM-11419] Reworking how we handle reacting to active fido2 requests to avoid ambiguity

* [PM-11419] Adjusting implementation to ensure we clear any active requests when the passkeys setting is modified
This commit is contained in:
Cesar Gonzalez 2024-09-09 07:44:08 -05:00 committed by GitHub
parent 3af0590807
commit 2827d338ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 288 additions and 84 deletions

View File

@ -217,6 +217,7 @@ export type OverlayBackgroundExtensionMessageHandlers = {
addEditCipherSubmitted: () => void; addEditCipherSubmitted: () => void;
editedCipher: () => void; editedCipher: () => void;
deletedCipher: () => void; deletedCipher: () => void;
fido2AbortRequest: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
}; };
export type PortMessageParam = { export type PortMessageParam = {

View File

@ -18,12 +18,12 @@ import {
EnvironmentService, EnvironmentService,
Region, Region,
} from "@bitwarden/common/platform/abstractions/environment.service"; } from "@bitwarden/common/platform/abstractions/environment.service";
import { Fido2ClientService } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
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 { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
import { Fido2ActiveRequestManager } from "@bitwarden/common/platform/services/fido2/fido2-active-request-manager";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { import {
FakeAccountService, FakeAccountService,
@ -32,6 +32,7 @@ import {
} from "@bitwarden/common/spec"; } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view";
@ -89,8 +90,9 @@ describe("OverlayBackground", () => {
let autofillSettingsService: MockProxy<AutofillSettingsService>; let autofillSettingsService: MockProxy<AutofillSettingsService>;
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;
let platformUtilsService: MockProxy<BrowserPlatformUtilsService>; let platformUtilsService: MockProxy<BrowserPlatformUtilsService>;
let availableAutofillCredentialsMock$: BehaviorSubject<Fido2CredentialView[]>; let enablePasskeysMock$: BehaviorSubject<boolean>;
let fido2ClientService: MockProxy<Fido2ClientService>; let vaultSettingsServiceMock: MockProxy<VaultSettingsService>;
let fido2ActiveRequestManager: Fido2ActiveRequestManager;
let selectedThemeMock$: BehaviorSubject<ThemeType>; let selectedThemeMock$: BehaviorSubject<ThemeType>;
let themeStateService: MockProxy<ThemeStateService>; let themeStateService: MockProxy<ThemeStateService>;
let overlayBackground: OverlayBackground; let overlayBackground: OverlayBackground;
@ -159,10 +161,10 @@ describe("OverlayBackground", () => {
autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$; autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$;
i18nService = mock<I18nService>(); i18nService = mock<I18nService>();
platformUtilsService = mock<BrowserPlatformUtilsService>(); platformUtilsService = mock<BrowserPlatformUtilsService>();
availableAutofillCredentialsMock$ = new BehaviorSubject([]); enablePasskeysMock$ = new BehaviorSubject(true);
fido2ClientService = mock<Fido2ClientService>({ vaultSettingsServiceMock = mock<VaultSettingsService>();
availableAutofillCredentials$: (_tabId) => availableAutofillCredentialsMock$, vaultSettingsServiceMock.enablePasskeys$ = enablePasskeysMock$;
}); fido2ActiveRequestManager = new Fido2ActiveRequestManager();
selectedThemeMock$ = new BehaviorSubject(ThemeType.Light); selectedThemeMock$ = new BehaviorSubject(ThemeType.Light);
themeStateService = mock<ThemeStateService>(); themeStateService = mock<ThemeStateService>();
themeStateService.selectedTheme$ = selectedThemeMock$; themeStateService.selectedTheme$ = selectedThemeMock$;
@ -176,7 +178,8 @@ describe("OverlayBackground", () => {
autofillSettingsService, autofillSettingsService,
i18nService, i18nService,
platformUtilsService, platformUtilsService,
fido2ClientService, vaultSettingsServiceMock,
fido2ActiveRequestManager,
themeStateService, themeStateService,
); );
portKeyForTabSpy = overlayBackground["portKeyForTab"]; portKeyForTabSpy = overlayBackground["portKeyForTab"];
@ -779,6 +782,15 @@ describe("OverlayBackground", () => {
expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled();
}); });
it("skips updating the inline menu ciphers if the current tab url has non-http protocol", async () => {
const nonHttpTab = createChromeTabMock({ url: "chrome-extension://id/route" });
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(nonHttpTab);
await overlayBackground.updateOverlayCiphers();
expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled();
});
it("closes the inline menu on the focused field's tab if the user's auth status is not unlocked", async () => { it("closes the inline menu on the focused field's tab if the user's auth status is not unlocked", async () => {
activeAccountStatusMock$.next(AuthenticationStatus.Locked); activeAccountStatusMock$.next(AuthenticationStatus.Locked);
const previousTab = mock<chrome.tabs.Tab>({ id: 1 }); const previousTab = mock<chrome.tabs.Tab>({ id: 1 });
@ -1113,7 +1125,11 @@ describe("OverlayBackground", () => {
}); });
it("adds available passkey ciphers to the inline menu", async () => { it("adds available passkey ciphers to the inline menu", async () => {
availableAutofillCredentialsMock$.next(passkeyCipher.login.fido2Credentials); void fido2ActiveRequestManager.newActiveRequest(
tab.id,
passkeyCipher.login.fido2Credentials,
new AbortController(),
);
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({
tabId: tab.id, tabId: tab.id,
filledByCipherType: CipherType.Login, filledByCipherType: CipherType.Login,
@ -1192,10 +1208,15 @@ describe("OverlayBackground", () => {
}); });
it("does not add a passkey to the inline menu when its rpId is part of the neverDomains exclusion list", async () => { it("does not add a passkey to the inline menu when its rpId is part of the neverDomains exclusion list", async () => {
availableAutofillCredentialsMock$.next(passkeyCipher.login.fido2Credentials); void fido2ActiveRequestManager.newActiveRequest(
tab.id,
passkeyCipher.login.fido2Credentials,
new AbortController(),
);
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({
tabId: tab.id, tabId: tab.id,
filledByCipherType: CipherType.Login, filledByCipherType: CipherType.Login,
showPasskeys: true,
}); });
cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, passkeyCipher]); cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, passkeyCipher]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
@ -1248,6 +1269,69 @@ describe("OverlayBackground", () => {
showPasskeysLabels: false, showPasskeysLabels: false,
}); });
}); });
it("does not add passkeys to the inline menu if the passkey setting is disabled", async () => {
enablePasskeysMock$.next(false);
void fido2ActiveRequestManager.newActiveRequest(
tab.id,
passkeyCipher.login.fido2Credentials,
new AbortController(),
);
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({
tabId: tab.id,
filledByCipherType: CipherType.Login,
showPasskeys: true,
});
cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, passkeyCipher]);
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
await overlayBackground.updateOverlayCiphers();
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateAutofillInlineMenuListCiphers",
ciphers: [
{
id: "inline-menu-cipher-0",
name: passkeyCipher.name,
type: CipherType.Login,
reprompt: passkeyCipher.reprompt,
favorite: passkeyCipher.favorite,
icon: {
fallbackImage: "images/bwi-globe.png",
icon: "bwi-globe",
image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png",
imageEnabled: true,
},
accountCreationFieldType: undefined,
login: {
username: passkeyCipher.login.username,
passkey: null,
},
},
{
id: "inline-menu-cipher-1",
name: loginCipher1.name,
type: CipherType.Login,
reprompt: loginCipher1.reprompt,
favorite: loginCipher1.favorite,
icon: {
fallbackImage: "images/bwi-globe.png",
icon: "bwi-globe",
image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png",
imageEnabled: true,
},
accountCreationFieldType: undefined,
login: {
username: loginCipher1.login.username,
passkey: null,
},
},
],
showInlineMenuAccountCreation: false,
showPasskeysLabels: false,
});
});
}); });
describe("extension message handlers", () => { describe("extension message handlers", () => {
@ -2537,6 +2621,25 @@ describe("OverlayBackground", () => {
}); });
}); });
}); });
describe("fido2AbortRequest", () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
it("removes an active request associated with the sender tab", () => {
const removeActiveRequestSpy = jest.spyOn(fido2ActiveRequestManager, "removeActiveRequest");
sendMockExtensionMessage({ command: "fido2AbortRequest" }, sender);
expect(removeActiveRequestSpy).toHaveBeenCalledWith(sender.tab.id);
});
it("updates the overlay ciphers after removing the active request", () => {
const updateOverlayCiphersSpy = jest.spyOn(overlayBackground, "updateOverlayCiphers");
sendMockExtensionMessage({ command: "fido2AbortRequest" }, sender);
expect(updateOverlayCiphersSpy).toHaveBeenCalledWith(false);
});
});
}); });
describe("handle extension onMessage", () => { describe("handle extension onMessage", () => {
@ -2920,6 +3023,7 @@ describe("OverlayBackground", () => {
[sender.frameId, pageDetailsForTab], [sender.frameId, pageDetailsForTab],
]); ]);
autofillService.isPasswordRepromptRequired.mockResolvedValue(false); autofillService.isPasswordRepromptRequired.mockResolvedValue(false);
jest.spyOn(fido2ActiveRequestManager, "getActiveRequest");
sendPortMessage(listMessageConnectorSpy, { sendPortMessage(listMessageConnectorSpy, {
command: "fillAutofillInlineMenuCipher", command: "fillAutofillInlineMenuCipher",
@ -2929,10 +3033,7 @@ describe("OverlayBackground", () => {
}); });
await flushPromises(); await flushPromises();
expect(fido2ClientService.autofillCredential).toHaveBeenCalledWith( expect(fido2ActiveRequestManager.getActiveRequest).toHaveBeenCalledWith(sender.tab.id);
sender.tab.id,
fido2Credential.credentialId,
);
}); });
}); });

View File

@ -6,6 +6,8 @@ import {
throttleTime, throttleTime,
switchMap, switchMap,
debounceTime, debounceTime,
Observable,
map,
} from "rxjs"; } from "rxjs";
import { parse } from "tldts"; import { parse } from "tldts";
@ -20,13 +22,17 @@ import { DomainSettingsService } from "@bitwarden/common/autofill/services/domai
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { Fido2ClientService } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction"; import {
Fido2ActiveRequestEvents,
Fido2ActiveRequestManager,
} from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.abstraction";
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon"; import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CardView } from "@bitwarden/common/vault/models/view/card.view";
@ -144,6 +150,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
addEditCipherSubmitted: () => this.updateOverlayCiphers(), addEditCipherSubmitted: () => this.updateOverlayCiphers(),
editedCipher: () => this.updateOverlayCiphers(), editedCipher: () => this.updateOverlayCiphers(),
deletedCipher: () => this.updateOverlayCiphers(), deletedCipher: () => this.updateOverlayCiphers(),
fido2AbortRequest: ({ sender }) => this.abortFido2ActiveRequest(sender),
}; };
private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = { private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = {
triggerDelayedAutofillInlineMenuClosure: () => this.triggerDelayedInlineMenuClosure(), triggerDelayedAutofillInlineMenuClosure: () => this.triggerDelayedInlineMenuClosure(),
@ -175,7 +182,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private autofillSettingsService: AutofillSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private fido2ClientService: Fido2ClientService, private vaultSettingsService: VaultSettingsService,
private fido2ActiveRequestManager: Fido2ActiveRequestManager,
private themeStateService: ThemeStateService, private themeStateService: ThemeStateService,
) { ) {
this.initOverlayEventObservables(); this.initOverlayEventObservables();
@ -196,7 +204,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
*/ */
private initOverlayEventObservables() { private initOverlayEventObservables() {
this.storeInlineMenuFido2CredentialsSubject this.storeInlineMenuFido2CredentialsSubject
.pipe(switchMap((tabId) => this.fido2ClientService.availableAutofillCredentials$(tabId))) .pipe(switchMap((tabId) => this.availablePasskeyAuthCredentials$(tabId)))
.subscribe((credentials) => this.storeInlineMenuFido2Credentials(credentials)); .subscribe((credentials) => this.storeInlineMenuFido2Credentials(credentials));
this.repositionInlineMenuSubject this.repositionInlineMenuSubject
.pipe( .pipe(
@ -279,6 +287,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return; return;
} }
const request = this.fido2ActiveRequestManager.getActiveRequest(currentTab.id);
if (request) {
request.subject.next({ type: Fido2ActiveRequestEvents.Refresh });
}
this.inlineMenuFido2Credentials.clear(); this.inlineMenuFido2Credentials.clear();
this.storeInlineMenuFido2CredentialsSubject.next(currentTab.id); this.storeInlineMenuFido2CredentialsSubject.next(currentTab.id);
@ -452,6 +465,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
if (domainExclusions) { if (domainExclusions) {
domainExclusionsSet = new Set(Object.keys(await this.getExcludedDomains())); domainExclusionsSet = new Set(Object.keys(await this.getExcludedDomains()));
} }
const passkeysEnabled = await firstValueFrom(this.vaultSettingsService.enablePasskeys$);
for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) { for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) {
const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex]; const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex];
@ -459,7 +473,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
continue; continue;
} }
if (!this.showCipherAsPasskey(cipher, domainExclusionsSet)) { if (!passkeysEnabled || !(await this.showCipherAsPasskey(cipher, domainExclusionsSet))) {
inlineMenuCipherData.push( inlineMenuCipherData.push(
this.buildCipherData({ inlineMenuCipherId, cipher, showFavicons }), this.buildCipherData({ inlineMenuCipherId, cipher, showFavicons }),
); );
@ -497,7 +511,10 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* @param cipher - The cipher to check * @param cipher - The cipher to check
* @param domainExclusions - The domain exclusions to check against * @param domainExclusions - The domain exclusions to check against
*/ */
private showCipherAsPasskey(cipher: CipherView, domainExclusions: Set<string> | null): boolean { private async showCipherAsPasskey(
cipher: CipherView,
domainExclusions: Set<string> | null,
): Promise<boolean> {
if (cipher.type !== CipherType.Login || !this.focusedFieldData?.showPasskeys) { if (cipher.type !== CipherType.Login || !this.focusedFieldData?.showPasskeys) {
return false; return false;
} }
@ -514,10 +531,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return false; return false;
} }
return ( return this.inlineMenuFido2Credentials.has(credentialId);
this.inlineMenuFido2Credentials.size === 0 ||
this.inlineMenuFido2Credentials.has(credentialId)
);
} }
/** /**
@ -635,12 +649,35 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* @param credentials - The FIDO2 credentials to store * @param credentials - The FIDO2 credentials to store
*/ */
private storeInlineMenuFido2Credentials(credentials: Fido2CredentialView[]) { private storeInlineMenuFido2Credentials(credentials: Fido2CredentialView[]) {
this.inlineMenuFido2Credentials.clear();
credentials.forEach( credentials.forEach(
(credential) => (credential) =>
credential?.credentialId && this.inlineMenuFido2Credentials.add(credential.credentialId), credential?.credentialId && this.inlineMenuFido2Credentials.add(credential.credentialId),
); );
} }
/**
* Gets the passkey credentials available from an active FIDO2 request for a given tab.
*
* @param tabId - The tab id to get the active request for.
*/
private availablePasskeyAuthCredentials$(tabId: number): Observable<Fido2CredentialView[]> {
return this.fido2ActiveRequestManager
.getActiveRequest$(tabId)
.pipe(map((request) => request?.credentials ?? []));
}
/**
* Aborts an active FIDO2 request for a given tab and updates the inline menu ciphers.
*
* @param sender - The sender of the message
*/
private async abortFido2ActiveRequest(sender: chrome.runtime.MessageSender) {
this.fido2ActiveRequestManager.removeActiveRequest(sender.tab.id);
await this.updateOverlayCiphers(false);
}
/** /**
* Gets the neverDomains setting from the domain settings service. * Gets the neverDomains setting from the domain settings service.
*/ */
@ -900,11 +937,12 @@ export class OverlayBackground implements OverlayBackgroundInterface {
const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId); const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId);
if (usePasskey && cipher.login?.hasFido2Credentials) { if (usePasskey && cipher.login?.hasFido2Credentials) {
await this.fido2ClientService.autofillCredential( await this.authenticatePasskeyCredential(
sender.tab.id, sender.tab.id,
cipher.login.fido2Credentials[0].credentialId, cipher.login.fido2Credentials[0].credentialId,
); );
this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher); this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher);
this.closeInlineMenu(sender, { forceCloseInlineMenu: true });
return; return;
} }
@ -927,6 +965,24 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher); this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher);
} }
/**
* Triggers a FIDO2 authentication from the inline menu using the passed credential ID.
*
* @param tabId - The tab ID to trigger the authentication for
* @param credentialId - The credential ID to authenticate
*/
async authenticatePasskeyCredential(tabId: number, credentialId: string) {
const request = this.fido2ActiveRequestManager.getActiveRequest(tabId);
if (!request) {
this.logService.error(
"Could not complete passkey autofill due to missing active Fido2 request",
);
return;
}
request.subject.next({ type: Fido2ActiveRequestEvents.Continue, credentialId });
}
/** /**
* Sets the most recently used cipher at the top of the list of ciphers. * Sets the most recently used cipher at the top of the list of ciphers.
* *

View File

@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { Fido2ActiveRequestManager } from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.abstraction";
import { import {
AssertCredentialParams, AssertCredentialParams,
CreateCredentialParams, CreateCredentialParams,
@ -52,6 +53,7 @@ describe("Fido2Background", () => {
let tabMock!: MockProxy<chrome.tabs.Tab>; let tabMock!: MockProxy<chrome.tabs.Tab>;
let senderMock!: MockProxy<chrome.runtime.MessageSender>; let senderMock!: MockProxy<chrome.runtime.MessageSender>;
let logService!: MockProxy<LogService>; let logService!: MockProxy<LogService>;
let fido2ActiveRequestManager: MockProxy<Fido2ActiveRequestManager>;
let fido2ClientService!: MockProxy<Fido2ClientService>; let fido2ClientService!: MockProxy<Fido2ClientService>;
let vaultSettingsService!: MockProxy<VaultSettingsService>; let vaultSettingsService!: MockProxy<VaultSettingsService>;
let scriptInjectorServiceMock!: MockProxy<BrowserScriptInjectorService>; let scriptInjectorServiceMock!: MockProxy<BrowserScriptInjectorService>;
@ -77,9 +79,11 @@ describe("Fido2Background", () => {
enablePasskeysMock$ = new BehaviorSubject(true); enablePasskeysMock$ = new BehaviorSubject(true);
vaultSettingsService.enablePasskeys$ = enablePasskeysMock$; vaultSettingsService.enablePasskeys$ = enablePasskeysMock$;
fido2ActiveRequestManager = mock<Fido2ActiveRequestManager>();
fido2ClientService.isFido2FeatureEnabled.mockResolvedValue(true); fido2ClientService.isFido2FeatureEnabled.mockResolvedValue(true);
fido2Background = new Fido2Background( fido2Background = new Fido2Background(
logService, logService,
fido2ActiveRequestManager,
fido2ClientService, fido2ClientService,
vaultSettingsService, vaultSettingsService,
scriptInjectorServiceMock, scriptInjectorServiceMock,

View File

@ -3,6 +3,7 @@ import { pairwise } from "rxjs/operators";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { Fido2ActiveRequestManager } from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.abstraction";
import { import {
AssertCredentialParams, AssertCredentialParams,
AssertCredentialResult, AssertCredentialResult,
@ -49,6 +50,7 @@ export class Fido2Background implements Fido2BackgroundInterface {
constructor( constructor(
private logService: LogService, private logService: LogService,
private fido2ActiveRequestManager: Fido2ActiveRequestManager,
private fido2ClientService: Fido2ClientService, private fido2ClientService: Fido2ClientService,
private vaultSettingsService: VaultSettingsService, private vaultSettingsService: VaultSettingsService,
private scriptInjectorService: ScriptInjectorService, private scriptInjectorService: ScriptInjectorService,
@ -96,6 +98,7 @@ export class Fido2Background implements Fido2BackgroundInterface {
previousEnablePasskeysSetting: boolean, previousEnablePasskeysSetting: boolean,
enablePasskeys: boolean, enablePasskeys: boolean,
) { ) {
this.fido2ActiveRequestManager.removeAllActiveRequests();
await this.updateContentScriptRegistration(); await this.updateContentScriptRegistration();
if (previousEnablePasskeysSetting === undefined) { if (previousEnablePasskeysSetting === undefined) {

View File

@ -60,6 +60,10 @@ import { MessageWithMetadata, Messenger } from "./messaging/messenger";
message.data as InsecureAssertCredentialParams, message.data as InsecureAssertCredentialParams,
); );
} }
if (message.type === MessageType.AbortRequest) {
return sendExtensionMessage("fido2AbortRequest", { abortedRequestId: requestId });
}
} finally { } finally {
abortController.signal.removeEventListener("abort", abortHandler); abortController.signal.removeEventListener("abort", abortHandler);
} }

View File

@ -131,6 +131,12 @@ import { Messenger } from "./messaging/messenger";
const internalAbortControllers = [new AbortController(), new AbortController()]; const internalAbortControllers = [new AbortController(), new AbortController()];
const bitwardenResponse = async (internalAbortController: AbortController) => { const bitwardenResponse = async (internalAbortController: AbortController) => {
try { try {
const abortListener = () =>
messenger.request({
type: MessageType.AbortRequest,
abortedRequestId: abortSignal.toString(),
});
internalAbortController.signal.addEventListener("abort", abortListener);
const response = await messenger.request( const response = await messenger.request(
{ {
type: MessageType.CredentialGetRequest, type: MessageType.CredentialGetRequest,
@ -138,6 +144,7 @@ import { Messenger } from "./messaging/messenger";
}, },
internalAbortController.signal, internalAbortController.signal,
); );
internalAbortController.signal.removeEventListener("abort", abortListener);
if (response.type !== MessageType.CredentialGetResponse) { if (response.type !== MessageType.CredentialGetResponse) {
throw new Error("Something went wrong."); throw new Error("Something went wrong.");
} }

View File

@ -242,12 +242,13 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
cipherIds, cipherIds,
userVerification, userVerification,
assumeUserPresence, assumeUserPresence,
masterPasswordRepromptRequired,
}: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { }: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
// NOTE: For now, we are defaulting to a userVerified status of `true` when the request // NOTE: For now, we are defaulting to a userVerified status of `true` when the request
// is for a conditionally mediated authentication. This will allow for mediated conditional // is for a conditionally mediated authentication. This will allow for mediated conditional
// authentication to function without requiring user interaction. This is a product // authentication to function without requiring user interaction. This is a product
// decision, rather than a decision based on the expected technical specifications. // decision, rather than a decision based on the expected technical specifications.
if (assumeUserPresence && cipherIds.length === 1) { if (assumeUserPresence && cipherIds.length === 1 && !masterPasswordRepromptRequired) {
return { cipherId: cipherIds[0], userVerified: userVerification }; return { cipherId: cipherIds[0], userVerified: userVerification };
} }

View File

@ -79,6 +79,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { Fido2ActiveRequestManager as Fido2ActiveRequestManagerAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.abstraction";
import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction"; import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction";
import { Fido2ClientService as Fido2ClientServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction"; import { Fido2ClientService as Fido2ClientServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction"; import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction";
@ -324,6 +325,7 @@ export default class MainBackground {
userVerificationApiService: UserVerificationApiServiceAbstraction; userVerificationApiService: UserVerificationApiServiceAbstraction;
fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction; fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction;
fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction; fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction;
fido2ActiveRequestManager: Fido2ActiveRequestManagerAbstraction;
fido2ClientService: Fido2ClientServiceAbstraction; fido2ClientService: Fido2ClientServiceAbstraction;
avatarService: AvatarServiceAbstraction; avatarService: AvatarServiceAbstraction;
mainContextMenuHandler: MainContextMenuHandler; mainContextMenuHandler: MainContextMenuHandler;
@ -1021,7 +1023,7 @@ export default class MainBackground {
this.accountService, this.accountService,
this.logService, this.logService,
); );
const fido2ActiveRequestManager = new Fido2ActiveRequestManager(); this.fido2ActiveRequestManager = new Fido2ActiveRequestManager();
this.fido2ClientService = new Fido2ClientService( this.fido2ClientService = new Fido2ClientService(
this.fido2AuthenticatorService, this.fido2AuthenticatorService,
this.configService, this.configService,
@ -1029,7 +1031,7 @@ export default class MainBackground {
this.vaultSettingsService, this.vaultSettingsService,
this.domainSettingsService, this.domainSettingsService,
this.taskSchedulerService, this.taskSchedulerService,
fido2ActiveRequestManager, this.fido2ActiveRequestManager,
this.logService, this.logService,
); );
@ -1057,6 +1059,7 @@ export default class MainBackground {
if (!this.popupOnlyContext) { if (!this.popupOnlyContext) {
this.fido2Background = new Fido2Background( this.fido2Background = new Fido2Background(
this.logService, this.logService,
this.fido2ActiveRequestManager,
this.fido2ClientService, this.fido2ClientService,
this.vaultSettingsService, this.vaultSettingsService,
this.scriptInjectorService, this.scriptInjectorService,
@ -1605,7 +1608,8 @@ export default class MainBackground {
this.autofillSettingsService, this.autofillSettingsService,
this.i18nService, this.i18nService,
this.platformUtilsService, this.platformUtilsService,
this.fido2ClientService, this.vaultSettingsService,
this.fido2ActiveRequestManager,
this.themeStateService, this.themeStateService,
); );
} }

View File

@ -2,9 +2,22 @@ import { Observable, Subject } from "rxjs";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view"; import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
export const Fido2ActiveRequestEvents = {
Refresh: "refresh-fido2-active-request",
Abort: "abort-fido2-active-request",
Continue: "continue-fido2-active-request",
} as const;
type Fido2ActiveRequestEvent = typeof Fido2ActiveRequestEvents;
export type RequestResult =
| { type: Fido2ActiveRequestEvent["Refresh"] }
| { type: Fido2ActiveRequestEvent["Abort"] }
| { type: Fido2ActiveRequestEvent["Continue"]; credentialId: string };
export interface ActiveRequest { export interface ActiveRequest {
credentials: Fido2CredentialView[]; credentials: Fido2CredentialView[];
subject: Subject<string>; subject: Subject<RequestResult>;
} }
export type RequestCollection = Readonly<{ [tabId: number]: ActiveRequest }>; export type RequestCollection = Readonly<{ [tabId: number]: ActiveRequest }>;
@ -16,6 +29,7 @@ export abstract class Fido2ActiveRequestManager {
tabId: number, tabId: number,
credentials: Fido2CredentialView[], credentials: Fido2CredentialView[],
abortController: AbortController, abortController: AbortController,
) => Promise<string>; ) => Promise<RequestResult>;
removeActiveRequest: (tabId: number) => void; removeActiveRequest: (tabId: number) => void;
removeAllActiveRequests: () => void;
} }

View File

@ -1,7 +1,3 @@
import { Observable } from "rxjs";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
export const UserRequestedFallbackAbortReason = "UserRequestedFallback"; export const UserRequestedFallbackAbortReason = "UserRequestedFallback";
export type UserVerification = "discouraged" | "preferred" | "required"; export type UserVerification = "discouraged" | "preferred" | "required";
@ -20,10 +16,6 @@ export type UserVerification = "discouraged" | "preferred" | "required";
export abstract class Fido2ClientService { export abstract class Fido2ClientService {
isFido2FeatureEnabled: (hostname: string, origin: string) => Promise<boolean>; isFido2FeatureEnabled: (hostname: string, origin: string) => Promise<boolean>;
availableAutofillCredentials$: (tabId: number) => Observable<Fido2CredentialView[]>;
autofillCredential: (tabId: number, credentialId: string) => Promise<void>;
/** /**
* Allows WebAuthn Relying Party scripts to request the creation of a new public key credential source. * Allows WebAuthn Relying Party scripts to request the creation of a new public key credential source.
* For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-createCredential * For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-createCredential

View File

@ -45,6 +45,11 @@ export interface PickCredentialParams {
* 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; assumeUserPresence?: boolean;
/**
* Identifies whether a cipher requires a master password reprompt when getting a credential.
*/
masterPasswordRepromptRequired?: boolean;
} }
/** /**

View File

@ -14,6 +14,8 @@ import {
ActiveRequest, ActiveRequest,
RequestCollection, RequestCollection,
Fido2ActiveRequestManager as Fido2ActiveRequestManagerAbstraction, Fido2ActiveRequestManager as Fido2ActiveRequestManagerAbstraction,
Fido2ActiveRequestEvents,
RequestResult,
} from "../../abstractions/fido2/fido2-active-request-manager.abstraction"; } from "../../abstractions/fido2/fido2-active-request-manager.abstraction";
export class Fido2ActiveRequestManager implements Fido2ActiveRequestManagerAbstraction { export class Fido2ActiveRequestManager implements Fido2ActiveRequestManagerAbstraction {
@ -53,7 +55,7 @@ export class Fido2ActiveRequestManager implements Fido2ActiveRequestManagerAbstr
tabId: number, tabId: number,
credentials: Fido2CredentialView[], credentials: Fido2CredentialView[],
abortController: AbortController, abortController: AbortController,
): Promise<string> { ): Promise<RequestResult> {
const newRequest: ActiveRequest = { const newRequest: ActiveRequest = {
credentials, credentials,
subject: new Subject(), subject: new Subject(),
@ -65,10 +67,10 @@ export class Fido2ActiveRequestManager implements Fido2ActiveRequestManagerAbstr
const abortListener = () => this.abortActiveRequest(tabId); const abortListener = () => this.abortActiveRequest(tabId);
abortController.signal.addEventListener("abort", abortListener); abortController.signal.addEventListener("abort", abortListener);
const credentialId = firstValueFrom(newRequest.subject); const requestResult = firstValueFrom(newRequest.subject);
abortController.signal.removeEventListener("abort", abortListener); abortController.signal.removeEventListener("abort", abortListener);
return credentialId; return requestResult;
} }
/** /**
@ -85,12 +87,23 @@ export class Fido2ActiveRequestManager implements Fido2ActiveRequestManagerAbstr
}); });
} }
/**
* Removes and aborts all active requests.
*/
removeAllActiveRequests() {
Object.keys(this.activeRequests$.value).forEach((tabId) => {
this.abortActiveRequest(Number(tabId));
});
this.updateRequests(() => ({}));
}
/** /**
* Aborts the active request associated with a given tab id. * Aborts the active request associated with a given tab id.
* *
* @param tabId - The tab id to abort the active request for. * @param tabId - The tab id to abort the active request for.
*/ */
private abortActiveRequest(tabId: number): void { private abortActiveRequest(tabId: number): void {
this.activeRequests$.value[tabId]?.subject.next({ type: Fido2ActiveRequestEvents.Abort });
this.activeRequests$.value[tabId]?.subject.error( this.activeRequests$.value[tabId]?.subject.error(
new DOMException("The operation either timed out or was not allowed.", "AbortError"), new DOMException("The operation either timed out or was not allowed.", "AbortError"),
); );

View File

@ -576,6 +576,7 @@ describe("FidoAuthenticatorService", () => {
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: ciphers.map((c) => c.id), cipherIds: ciphers.map((c) => c.id),
userVerification: false, userVerification: false,
masterPasswordRepromptRequired: false,
}); });
}); });
@ -592,6 +593,7 @@ describe("FidoAuthenticatorService", () => {
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: [discoverableCiphers[0].id], cipherIds: [discoverableCiphers[0].id],
userVerification: false, userVerification: false,
masterPasswordRepromptRequired: false,
}); });
}); });
@ -609,6 +611,7 @@ describe("FidoAuthenticatorService", () => {
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: ciphers.map((c) => c.id), cipherIds: ciphers.map((c) => c.id),
userVerification, userVerification,
masterPasswordRepromptRequired: false,
}); });
}); });
} }

View File

@ -160,6 +160,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
} }
const reencrypted = await this.cipherService.encrypt(cipher, activeUserId); const reencrypted = await this.cipherService.encrypt(cipher, activeUserId);
await this.cipherService.updateWithServer(reencrypted); await this.cipherService.updateWithServer(reencrypted);
await this.cipherService.clearCache(activeUserId);
credentialId = fido2Credential.credentialId; credentialId = fido2Credential.credentialId;
} catch (error) { } catch (error) {
this.logService?.error( this.logService?.error(
@ -243,12 +244,18 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
} }
let response = { cipherId: cipherOptions[0].id, userVerified: false }; let response = { cipherId: cipherOptions[0].id, userVerified: false };
const masterPasswordRepromptRequired = cipherOptions.some(
(cipher) => cipher.reprompt !== CipherRepromptType.None,
);
if (this.requiresUserVerificationPrompt(params, cipherOptions)) { if (
this.requiresUserVerificationPrompt(params, cipherOptions, masterPasswordRepromptRequired)
) {
response = await userInterfaceSession.pickCredential({ response = await userInterfaceSession.pickCredential({
cipherIds: cipherOptions.map((cipher) => cipher.id), cipherIds: cipherOptions.map((cipher) => cipher.id),
userVerification: params.requireUserVerification, userVerification: params.requireUserVerification,
assumeUserPresence: params.assumeUserPresence, assumeUserPresence: params.assumeUserPresence,
masterPasswordRepromptRequired,
}); });
} }
@ -292,6 +299,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
); );
const encrypted = await this.cipherService.encrypt(selectedCipher, activeUserId); const encrypted = await this.cipherService.encrypt(selectedCipher, activeUserId);
await this.cipherService.updateWithServer(encrypted); await this.cipherService.updateWithServer(encrypted);
await this.cipherService.clearCache(activeUserId);
} }
const authenticatorData = await generateAuthData({ const authenticatorData = await generateAuthData({
@ -330,13 +338,14 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
private requiresUserVerificationPrompt( private requiresUserVerificationPrompt(
params: Fido2AuthenticatorGetAssertionParams, params: Fido2AuthenticatorGetAssertionParams,
cipherOptions: CipherView[], cipherOptions: CipherView[],
masterPasswordRepromptRequired: boolean,
): boolean { ): boolean {
return ( return (
params.requireUserVerification || params.requireUserVerification ||
!params.assumeUserPresence || !params.assumeUserPresence ||
cipherOptions.length > 1 || cipherOptions.length > 1 ||
cipherOptions.length === 0 || cipherOptions.length === 0 ||
cipherOptions.some((cipher) => cipher.reprompt !== CipherRepromptType.None) masterPasswordRepromptRequired
); );
} }

View File

@ -6,11 +6,12 @@ import { AuthenticationStatus } from "../../../auth/enums/authentication-status"
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { Utils } from "../../../platform/misc/utils"; import { Utils } from "../../../platform/misc/utils";
import { VaultSettingsService } from "../../../vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "../../../vault/abstractions/vault-settings/vault-settings.service";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
import { ConfigService } from "../../abstractions/config/config.service"; import { ConfigService } from "../../abstractions/config/config.service";
import { import {
ActiveRequest, ActiveRequest,
Fido2ActiveRequestEvents,
Fido2ActiveRequestManager, Fido2ActiveRequestManager,
RequestResult,
} from "../../abstractions/fido2/fido2-active-request-manager.abstraction"; } from "../../abstractions/fido2/fido2-active-request-manager.abstraction";
import { import {
Fido2AuthenticatorError, Fido2AuthenticatorError,
@ -56,7 +57,10 @@ describe("FidoAuthenticatorService", () => {
domainSettingsService = mock<DomainSettingsService>(); domainSettingsService = mock<DomainSettingsService>();
taskSchedulerService = mock<TaskSchedulerService>(); taskSchedulerService = mock<TaskSchedulerService>();
activeRequest = mock<ActiveRequest>({ activeRequest = mock<ActiveRequest>({
subject: new BehaviorSubject<string>(""), subject: new BehaviorSubject<RequestResult>({
type: Fido2ActiveRequestEvents.Continue,
credentialId: "",
}),
}); });
requestManager = mock<Fido2ActiveRequestManager>({ requestManager = mock<Fido2ActiveRequestManager>({
getActiveRequest$: (tabId: number) => new BehaviorSubject(activeRequest), getActiveRequest$: (tabId: number) => new BehaviorSubject(activeRequest),
@ -615,7 +619,10 @@ describe("FidoAuthenticatorService", () => {
}); });
beforeEach(() => { beforeEach(() => {
requestManager.newActiveRequest.mockResolvedValue(crypto.randomUUID()); requestManager.newActiveRequest.mockResolvedValue({
type: Fido2ActiveRequestEvents.Continue,
credentialId: crypto.randomUUID(),
});
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult()); authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
}); });
@ -676,28 +683,6 @@ describe("FidoAuthenticatorService", () => {
}; };
} }
}); });
describe("autofill of credentials through the active request manager", () => {
it("returns an observable that updates with an array of the credentials for active Fido2 requests", async () => {
const activeRequestCredentials = mock<Fido2CredentialView>();
activeRequest.credentials = [activeRequestCredentials];
const observable = client.availableAutofillCredentials$(tab.id);
observable.subscribe((credentials) => {
expect(credentials).toEqual([activeRequestCredentials]);
});
});
it("triggers the logic of the next behavior subject of an active request", async () => {
const activeRequestCredentials = mock<Fido2CredentialView>();
activeRequest.credentials = [activeRequestCredentials];
jest.spyOn(activeRequest.subject, "next");
await client.autofillCredential(tab.id, activeRequestCredentials.credentialId);
expect(activeRequest.subject.next).toHaveBeenCalled();
});
});
}); });
/** This is a fake function that always returns the same byte sequence */ /** This is a fake function that always returns the same byte sequence */

View File

@ -1,13 +1,15 @@
import { firstValueFrom, map, Observable, Subscription } from "rxjs"; import { firstValueFrom, Subscription } from "rxjs";
import { parse } from "tldts"; import { parse } from "tldts";
import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { VaultSettingsService } from "../../../vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "../../../vault/abstractions/vault-settings/vault-settings.service";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
import { ConfigService } from "../../abstractions/config/config.service"; import { ConfigService } from "../../abstractions/config/config.service";
import { Fido2ActiveRequestManager } from "../../abstractions/fido2/fido2-active-request-manager.abstraction"; import {
Fido2ActiveRequestEvents,
Fido2ActiveRequestManager,
} from "../../abstractions/fido2/fido2-active-request-manager.abstraction";
import { import {
Fido2AuthenticatorError, Fido2AuthenticatorError,
Fido2AuthenticatorErrorCode, Fido2AuthenticatorErrorCode,
@ -73,17 +75,6 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
); );
} }
availableAutofillCredentials$(tabId: number): Observable<Fido2CredentialView[]> {
return this.requestManager
.getActiveRequest$(tabId)
.pipe(map((request) => request?.credentials ?? []));
}
async autofillCredential(tabId: number, credentialId: string) {
const request = this.requestManager.getActiveRequest(tabId);
request.subject.next(credentialId);
}
async isFido2FeatureEnabled(hostname: string, origin: string): Promise<boolean> { async isFido2FeatureEnabled(hostname: string, origin: string): Promise<boolean> {
const isUserLoggedIn = const isUserLoggedIn =
(await this.authService.getAuthStatus()) !== AuthenticationStatus.LoggedOut; (await this.authService.getAuthStatus()) !== AuthenticationStatus.LoggedOut;
@ -385,12 +376,23 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
this.logService?.info( this.logService?.info(
`[Fido2Client] started mediated request, available credentials: ${availableCredentials.length}`, `[Fido2Client] started mediated request, available credentials: ${availableCredentials.length}`,
); );
const credentialId = await this.requestManager.newActiveRequest( const requestResult = await this.requestManager.newActiveRequest(
tab.id, tab.id,
availableCredentials, availableCredentials,
abortController, abortController,
); );
params.allowedCredentialIds = [Fido2Utils.bufferToString(guidToRawFormat(credentialId))];
if (requestResult.type === Fido2ActiveRequestEvents.Refresh) {
continue;
}
if (requestResult.type === Fido2ActiveRequestEvents.Abort) {
break;
}
params.allowedCredentialIds = [
Fido2Utils.bufferToString(guidToRawFormat(requestResult.credentialId)),
];
assumeUserPresence = true; assumeUserPresence = true;
const clientDataHash = await crypto.subtle.digest({ name: "SHA-256" }, clientDataJSONBytes); const clientDataHash = await crypto.subtle.digest({ name: "SHA-256" }, clientDataJSONBytes);