mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-11 10:10:25 +01: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:
parent
3af0590807
commit
2827d338ee
@ -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 = {
|
||||||
|
@ -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,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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.");
|
||||||
}
|
}
|
||||||
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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"),
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 */
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user