1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-9473] Add messaging for macOS passkey extension and desktop (#10768)

* Add messaging for macos passkey provider

* fix: credential id conversion

* Make build.sh executable

Co-authored-by: Colton Hurst <colton@coltonhurst.com>

* chore: add TODO

---------

Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
This commit is contained in:
Daniel García
2024-12-19 09:00:21 +01:00
committed by GitHub
parent 456046e095
commit 51f6594d4b
37 changed files with 1935 additions and 149 deletions

View File

@@ -8,7 +8,7 @@ import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential
*
* The authenticator provides key management and cryptographic signatures.
*/
export abstract class Fido2AuthenticatorService {
export abstract class Fido2AuthenticatorService<ParentWindowReference> {
/**
* Create and save a new credential as described in:
* https://www.w3.org/TR/webauthn-3/#sctn-op-make-cred
@@ -19,7 +19,7 @@ export abstract class Fido2AuthenticatorService {
**/
makeCredential: (
params: Fido2AuthenticatorMakeCredentialsParams,
tab: chrome.tabs.Tab,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<Fido2AuthenticatorMakeCredentialResult>;
@@ -33,7 +33,7 @@ export abstract class Fido2AuthenticatorService {
*/
getAssertion: (
params: Fido2AuthenticatorGetAssertionParams,
tab: chrome.tabs.Tab,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<Fido2AuthenticatorGetAssertionResult>;

View File

@@ -15,7 +15,7 @@ export type UserVerification = "discouraged" | "preferred" | "required";
* It is responsible for both marshalling the inputs for the underlying authenticator operations,
* and for returning the results of the latter operations to the Web Authentication API's callers.
*/
export abstract class Fido2ClientService {
export abstract class Fido2ClientService<ParentWindowReference> {
isFido2FeatureEnabled: (hostname: string, origin: string) => Promise<boolean>;
/**
@@ -28,7 +28,7 @@ export abstract class Fido2ClientService {
*/
createCredential: (
params: CreateCredentialParams,
tab: chrome.tabs.Tab,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<CreateCredentialResult>;
@@ -43,7 +43,7 @@ export abstract class Fido2ClientService {
*/
assertCredential: (
params: AssertCredentialParams,
tab: chrome.tabs.Tab,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<AssertCredentialResult>;
}

View File

@@ -61,7 +61,7 @@ export interface PickCredentialParams {
* The service is session based and is intended to be used by the FIDO2 authenticator to open a window,
* and then use this window to ask the user for input and/or display messages to the user.
*/
export abstract class Fido2UserInterfaceService {
export abstract class Fido2UserInterfaceService<ParentWindowReference> {
/**
* Creates a new session.
* Note: This will not necessarily open a window until it is needed to request something from the user.
@@ -71,7 +71,7 @@ export abstract class Fido2UserInterfaceService {
*/
newSession: (
fallbackSupported: boolean,
tab: chrome.tabs.Tab,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<Fido2UserInterfaceSession>;
}

View File

@@ -30,6 +30,8 @@ import { parseCredentialId } from "./credential-id-utils";
import { AAGUID, Fido2AuthenticatorService } from "./fido2-authenticator.service";
import { Fido2Utils } from "./fido2-utils";
type ParentWindowReference = string;
const RpId = "bitwarden.com";
describe("FidoAuthenticatorService", () => {
@@ -41,16 +43,16 @@ describe("FidoAuthenticatorService", () => {
});
let cipherService!: MockProxy<CipherService>;
let userInterface!: MockProxy<Fido2UserInterfaceService>;
let userInterface!: MockProxy<Fido2UserInterfaceService<ParentWindowReference>>;
let userInterfaceSession!: MockProxy<Fido2UserInterfaceSession>;
let syncService!: MockProxy<SyncService>;
let accountService!: MockProxy<AccountService>;
let authenticator!: Fido2AuthenticatorService;
let tab!: chrome.tabs.Tab;
let authenticator!: Fido2AuthenticatorService<ParentWindowReference>;
let windowReference!: ParentWindowReference;
beforeEach(async () => {
cipherService = mock<CipherService>();
userInterface = mock<Fido2UserInterfaceService>();
userInterface = mock<Fido2UserInterfaceService<ParentWindowReference>>();
userInterfaceSession = mock<Fido2UserInterfaceSession>();
userInterface.newSession.mockResolvedValue(userInterfaceSession);
syncService = mock<SyncService>({
@@ -63,7 +65,7 @@ describe("FidoAuthenticatorService", () => {
syncService,
accountService,
);
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab;
windowReference = Utils.newGuid();
accountService.activeAccount$ = activeAccountSubject;
});
@@ -78,19 +80,21 @@ describe("FidoAuthenticatorService", () => {
// Spec: Check if at least one of the specified combinations of PublicKeyCredentialType and cryptographic parameters in credTypesAndPubKeyAlgs is supported. If not, return an error code equivalent to "NotSupportedError" and terminate the operation.
it("should throw error when input does not contain any supported algorithms", async () => {
const result = async () =>
await authenticator.makeCredential(invalidParams.unsupportedAlgorithm, tab);
await authenticator.makeCredential(invalidParams.unsupportedAlgorithm, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotSupported);
});
it("should throw error when requireResidentKey has invalid value", async () => {
const result = async () => await authenticator.makeCredential(invalidParams.invalidRk, tab);
const result = async () =>
await authenticator.makeCredential(invalidParams.invalidRk, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
});
it("should throw error when requireUserVerification has invalid value", async () => {
const result = async () => await authenticator.makeCredential(invalidParams.invalidUv, tab);
const result = async () =>
await authenticator.makeCredential(invalidParams.invalidUv, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
});
@@ -103,7 +107,7 @@ describe("FidoAuthenticatorService", () => {
it.skip("should throw error if requireUserVerification is set to true", async () => {
const params = await createParams({ requireUserVerification: true });
const result = async () => await authenticator.makeCredential(params, tab);
const result = async () => await authenticator.makeCredential(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Constraint);
});
@@ -117,7 +121,7 @@ describe("FidoAuthenticatorService", () => {
for (const p of Object.values(invalidParams)) {
try {
await authenticator.makeCredential(p, tab);
await authenticator.makeCredential(p, windowReference);
// eslint-disable-next-line no-empty
} catch {}
}
@@ -158,7 +162,7 @@ describe("FidoAuthenticatorService", () => {
userInterfaceSession.informExcludedCredential.mockResolvedValue();
try {
await authenticator.makeCredential(params, tab);
await authenticator.makeCredential(params, windowReference);
// eslint-disable-next-line no-empty
} catch {}
@@ -169,7 +173,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw error", async () => {
userInterfaceSession.informExcludedCredential.mockResolvedValue();
const result = async () => await authenticator.makeCredential(params, tab);
const result = async () => await authenticator.makeCredential(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
});
@@ -180,7 +184,7 @@ describe("FidoAuthenticatorService", () => {
excludedCipher.organizationId = "someOrganizationId";
try {
await authenticator.makeCredential(params, tab);
await authenticator.makeCredential(params, windowReference);
// eslint-disable-next-line no-empty
} catch {}
@@ -193,7 +197,7 @@ describe("FidoAuthenticatorService", () => {
for (const p of Object.values(invalidParams)) {
try {
await authenticator.makeCredential(p, tab);
await authenticator.makeCredential(p, windowReference);
// eslint-disable-next-line no-empty
} catch {}
}
@@ -230,7 +234,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: userVerification,
});
await authenticator.makeCredential(params, tab);
await authenticator.makeCredential(params, windowReference);
expect(userInterfaceSession.confirmNewCredential).toHaveBeenCalledWith({
credentialName: params.rpEntity.name,
@@ -250,7 +254,7 @@ describe("FidoAuthenticatorService", () => {
});
cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher);
await authenticator.makeCredential(params, tab);
await authenticator.makeCredential(params, windowReference);
const saved = cipherService.encrypt.mock.lastCall?.[0];
expect(saved).toEqual(
@@ -288,7 +292,7 @@ describe("FidoAuthenticatorService", () => {
});
const params = await createParams();
const result = async () => await authenticator.makeCredential(params, tab);
const result = async () => await authenticator.makeCredential(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
});
@@ -302,7 +306,7 @@ describe("FidoAuthenticatorService", () => {
const encryptedCipher = { ...existingCipher, reprompt: CipherRepromptType.Password };
cipherService.get.mockResolvedValue(encryptedCipher as unknown as Cipher);
const result = async () => await authenticator.makeCredential(params, tab);
const result = async () => await authenticator.makeCredential(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
});
@@ -317,7 +321,7 @@ describe("FidoAuthenticatorService", () => {
cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher);
cipherService.updateWithServer.mockRejectedValue(new Error("Internal error"));
const result = async () => await authenticator.makeCredential(params, tab);
const result = async () => await authenticator.makeCredential(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
});
@@ -358,7 +362,7 @@ describe("FidoAuthenticatorService", () => {
});
it("should return attestation object", async () => {
const result = await authenticator.makeCredential(params, tab);
const result = await authenticator.makeCredential(params, windowReference);
const attestationObject = CBOR.decode(
Fido2Utils.bufferSourceToUint8Array(result.attestationObject).buffer,
@@ -455,7 +459,8 @@ describe("FidoAuthenticatorService", () => {
describe("invalid input parameters", () => {
it("should throw error when requireUserVerification has invalid value", async () => {
const result = async () => await authenticator.getAssertion(invalidParams.invalidUv, tab);
const result = async () =>
await authenticator.getAssertion(invalidParams.invalidUv, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
});
@@ -468,7 +473,7 @@ describe("FidoAuthenticatorService", () => {
it.skip("should throw error if requireUserVerification is set to true", async () => {
const params = await createParams({ requireUserVerification: true });
const result = async () => await authenticator.getAssertion(params, tab);
const result = async () => await authenticator.getAssertion(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Constraint);
});
@@ -498,7 +503,7 @@ describe("FidoAuthenticatorService", () => {
userInterfaceSession.informCredentialNotFound.mockResolvedValue();
try {
await authenticator.getAssertion(params, tab);
await authenticator.getAssertion(params, windowReference);
// eslint-disable-next-line no-empty
} catch {}
@@ -513,7 +518,7 @@ describe("FidoAuthenticatorService", () => {
userInterfaceSession.informCredentialNotFound.mockResolvedValue();
try {
await authenticator.getAssertion(params, tab);
await authenticator.getAssertion(params, windowReference);
// eslint-disable-next-line no-empty
} catch {}
@@ -534,7 +539,7 @@ describe("FidoAuthenticatorService", () => {
/** Spec: If credentialOptions is now empty, return an error code equivalent to "NotAllowedError" and terminate the operation. */
it("should throw error", async () => {
const result = async () => await authenticator.getAssertion(params, tab);
const result = async () => await authenticator.getAssertion(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
});
@@ -573,7 +578,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: false,
});
await authenticator.getAssertion(params, tab);
await authenticator.getAssertion(params, windowReference);
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: ciphers.map((c) => c.id),
@@ -590,7 +595,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: false,
});
await authenticator.getAssertion(params, tab);
await authenticator.getAssertion(params, windowReference);
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: [discoverableCiphers[0].id],
@@ -608,7 +613,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: userVerification,
});
await authenticator.getAssertion(params, tab);
await authenticator.getAssertion(params, windowReference);
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: ciphers.map((c) => c.id),
@@ -625,7 +630,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: false,
});
const result = async () => await authenticator.getAssertion(params, tab);
const result = async () => await authenticator.getAssertion(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
});
@@ -637,7 +642,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: false,
});
const result = async () => await authenticator.getAssertion(params, tab);
const result = async () => await authenticator.getAssertion(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
});
@@ -686,7 +691,7 @@ describe("FidoAuthenticatorService", () => {
cipherService.encrypt.mockResolvedValue(encrypted as any);
ciphers[0].login.fido2Credentials[0].counter = 9000;
await authenticator.getAssertion(params, tab);
await authenticator.getAssertion(params, windowReference);
expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted);
expect(cipherService.encrypt).toHaveBeenCalledWith(
@@ -710,13 +715,13 @@ describe("FidoAuthenticatorService", () => {
cipherService.encrypt.mockResolvedValue(encrypted as any);
ciphers[0].login.fido2Credentials[0].counter = 0;
await authenticator.getAssertion(params, tab);
await authenticator.getAssertion(params, windowReference);
expect(cipherService.updateWithServer).not.toHaveBeenCalled();
});
it("should return an assertion result", async () => {
const result = await authenticator.getAssertion(params, tab);
const result = await authenticator.getAssertion(params, windowReference);
const encAuthData = result.authenticatorData;
const rpIdHash = encAuthData.slice(0, 32);
@@ -757,7 +762,7 @@ describe("FidoAuthenticatorService", () => {
for (let i = 0; i < 10; ++i) {
await init(); // Reset inputs
const result = await authenticator.getAssertion(params, tab);
const result = await authenticator.getAssertion(params, windowReference);
const counter = result.authenticatorData.slice(33, 37);
expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // double check that the counter doesn't change
@@ -774,7 +779,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw unkown error if creation fails", async () => {
cipherService.updateWithServer.mockRejectedValue(new Error("Internal error"));
const result = async () => await authenticator.getAssertion(params, tab);
const result = async () => await authenticator.getAssertion(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
});

View File

@@ -43,10 +43,12 @@ const KeyUsages: KeyUsage[] = ["sign"];
*
* It is highly recommended that the W3C specification is used a reference when reading this code.
*/
export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstraction {
export class Fido2AuthenticatorService<ParentWindowReference>
implements Fido2AuthenticatorServiceAbstraction<ParentWindowReference>
{
constructor(
private cipherService: CipherService,
private userInterface: Fido2UserInterfaceService,
private userInterface: Fido2UserInterfaceService<ParentWindowReference>,
private syncService: SyncService,
private accountService: AccountService,
private logService?: LogService,
@@ -54,12 +56,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
async makeCredential(
params: Fido2AuthenticatorMakeCredentialsParams,
tab: chrome.tabs.Tab,
window: ParentWindowReference,
abortController?: AbortController,
): Promise<Fido2AuthenticatorMakeCredentialResult> {
const userInterfaceSession = await this.userInterface.newSession(
params.fallbackSupported,
tab,
window,
abortController,
);
@@ -209,12 +211,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
async getAssertion(
params: Fido2AuthenticatorGetAssertionParams,
tab: chrome.tabs.Tab,
window: ParentWindowReference,
abortController?: AbortController,
): Promise<Fido2AuthenticatorGetAssertionResult> {
const userInterfaceSession = await this.userInterface.newSession(
params.fallbackSupported,
tab,
window,
abortController,
);
try {

View File

@@ -3,6 +3,9 @@
import { CipherType } from "../../../vault/enums";
import { CipherView } from "../../../vault/models/view/cipher.view";
import { Fido2CredentialAutofillView } from "../../../vault/models/view/fido2-credential-autofill.view";
import { Utils } from "../../misc/utils";
import { parseCredentialId } from "./credential-id-utils";
// TODO: Move into Fido2AuthenticatorService
export async function getCredentialsForAutofill(
@@ -15,9 +18,14 @@ export async function getCredentialsForAutofill(
)
.map((cipher) => {
const credential = cipher.login.fido2Credentials[0];
// Credentials are stored as a GUID or b64 string with `b64.` prepended,
// but we need to return them as a URL-safe base64 string
const credId = Utils.fromBufferToUrlB64(parseCredentialId(credential.credentialId));
return {
cipherId: cipher.id,
credentialId: credential.credentialId,
credentialId: credId,
rpId: credential.rpId,
userHandle: credential.userHandle,
userName: credential.userName,

View File

@@ -32,12 +32,14 @@ import { Fido2ClientService } from "./fido2-client.service";
import { Fido2Utils } from "./fido2-utils";
import { guidToRawFormat } from "./guid-utils";
type ParentWindowReference = string;
const RpId = "bitwarden.com";
const Origin = "https://bitwarden.com";
const VaultUrl = "https://vault.bitwarden.com";
describe("FidoAuthenticatorService", () => {
let authenticator!: MockProxy<Fido2AuthenticatorService>;
let authenticator!: MockProxy<Fido2AuthenticatorService<ParentWindowReference>>;
let configService!: MockProxy<ConfigService>;
let authService!: MockProxy<AuthService>;
let vaultSettingsService: MockProxy<VaultSettingsService>;
@@ -45,12 +47,12 @@ describe("FidoAuthenticatorService", () => {
let taskSchedulerService: MockProxy<TaskSchedulerService>;
let activeRequest!: MockProxy<ActiveRequest>;
let requestManager!: MockProxy<Fido2ActiveRequestManager>;
let client!: Fido2ClientService;
let tab!: chrome.tabs.Tab;
let client!: Fido2ClientService<ParentWindowReference>;
let windowReference!: ParentWindowReference;
let isValidRpId!: jest.SpyInstance;
beforeEach(async () => {
authenticator = mock<Fido2AuthenticatorService>();
authenticator = mock<Fido2AuthenticatorService<ParentWindowReference>>();
configService = mock<ConfigService>();
authService = mock<AuthService>();
vaultSettingsService = mock<VaultSettingsService>();
@@ -82,7 +84,7 @@ describe("FidoAuthenticatorService", () => {
vaultSettingsService.enablePasskeys$ = of(true);
domainSettingsService.neverDomains$ = of({});
authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab;
windowReference = Utils.newGuid();
});
afterEach(() => {
@@ -95,7 +97,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw error if sameOriginWithAncestors is false", async () => {
const params = createParams({ sameOriginWithAncestors: false });
const result = async () => await client.createCredential(params, tab);
const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotAllowedError" });
@@ -106,7 +108,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw error if user.id is too small", async () => {
const params = createParams({ user: { id: "", displayName: "displayName", name: "name" } });
const result = async () => await client.createCredential(params, tab);
const result = async () => await client.createCredential(params, windowReference);
await expect(result).rejects.toBeInstanceOf(TypeError);
});
@@ -121,7 +123,7 @@ describe("FidoAuthenticatorService", () => {
},
});
const result = async () => await client.createCredential(params, tab);
const result = async () => await client.createCredential(params, windowReference);
await expect(result).rejects.toBeInstanceOf(TypeError);
});
@@ -136,7 +138,7 @@ describe("FidoAuthenticatorService", () => {
origin: "invalid-domain-name",
});
const result = async () => await client.createCredential(params, tab);
const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
@@ -151,7 +153,7 @@ describe("FidoAuthenticatorService", () => {
rp: { id: "bitwarden.com", name: "Bitwarden" },
});
const result = async () => await client.createCredential(params, tab);
const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
@@ -165,7 +167,7 @@ describe("FidoAuthenticatorService", () => {
// `params` actually has a valid rp.id, but we're mocking the function to return false
isValidRpId.mockReturnValue(false);
const result = async () => await client.createCredential(params, tab);
const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
@@ -179,7 +181,7 @@ describe("FidoAuthenticatorService", () => {
});
domainSettingsService.neverDomains$ = of({ "bitwarden.com": null });
const result = async () => await client.createCredential(params, tab);
const result = async () => await client.createCredential(params, windowReference);
await expect(result).rejects.toThrow(FallbackRequestedError);
});
@@ -190,7 +192,7 @@ describe("FidoAuthenticatorService", () => {
rp: { id: "bitwarden.com", name: "Bitwarden" },
});
const result = async () => await client.createCredential(params, tab);
const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
@@ -204,7 +206,7 @@ describe("FidoAuthenticatorService", () => {
});
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
await client.createCredential(params, tab);
await client.createCredential(params, windowReference);
});
// Spec: If credTypesAndPubKeyAlgs is empty, return a DOMException whose name is "NotSupportedError", and terminate this algorithm.
@@ -216,7 +218,7 @@ describe("FidoAuthenticatorService", () => {
],
});
const result = async () => await client.createCredential(params, tab);
const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotSupportedError" });
@@ -231,7 +233,8 @@ describe("FidoAuthenticatorService", () => {
const abortController = new AbortController();
abortController.abort();
const result = async () => await client.createCredential(params, tab, abortController);
const result = async () =>
await client.createCredential(params, windowReference, abortController);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "AbortError" });
@@ -246,7 +249,7 @@ describe("FidoAuthenticatorService", () => {
});
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
await client.createCredential(params, tab);
await client.createCredential(params, windowReference);
expect(authenticator.makeCredential).toHaveBeenCalledWith(
expect.objectContaining({
@@ -259,7 +262,7 @@ describe("FidoAuthenticatorService", () => {
displayName: params.user.displayName,
}),
}),
tab,
windowReference,
expect.anything(),
);
});
@@ -271,7 +274,7 @@ describe("FidoAuthenticatorService", () => {
});
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
const result = await client.createCredential(params, tab);
const result = await client.createCredential(params, windowReference);
expect(result.extensions.credProps?.rk).toBe(true);
});
@@ -283,7 +286,7 @@ describe("FidoAuthenticatorService", () => {
});
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
const result = await client.createCredential(params, tab);
const result = await client.createCredential(params, windowReference);
expect(result.extensions.credProps?.rk).toBe(false);
});
@@ -295,7 +298,7 @@ describe("FidoAuthenticatorService", () => {
});
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
const result = await client.createCredential(params, tab);
const result = await client.createCredential(params, windowReference);
expect(result.extensions.credProps).toBeUndefined();
});
@@ -307,7 +310,7 @@ describe("FidoAuthenticatorService", () => {
new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.InvalidState),
);
const result = async () => await client.createCredential(params, tab);
const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "InvalidStateError" });
@@ -319,7 +322,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams();
authenticator.makeCredential.mockRejectedValue(new Error("unknown error"));
const result = async () => await client.createCredential(params, tab);
const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotAllowedError" });
@@ -330,7 +333,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams();
vaultSettingsService.enablePasskeys$ = of(false);
const result = async () => await client.createCredential(params, tab);
const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
@@ -340,7 +343,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams();
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut);
const result = async () => await client.createCredential(params, tab);
const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
@@ -349,7 +352,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw FallbackRequestedError if origin equals the bitwarden vault", async () => {
const params = createParams({ origin: VaultUrl });
const result = async () => await client.createCredential(params, tab);
const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
@@ -408,7 +411,7 @@ describe("FidoAuthenticatorService", () => {
origin: "invalid-domain-name",
});
const result = async () => await client.assertCredential(params, tab);
const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
@@ -423,7 +426,7 @@ describe("FidoAuthenticatorService", () => {
rpId: "bitwarden.com",
});
const result = async () => await client.assertCredential(params, tab);
const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
@@ -437,7 +440,7 @@ describe("FidoAuthenticatorService", () => {
// `params` actually has a valid rp.id, but we're mocking the function to return false
isValidRpId.mockReturnValue(false);
const result = async () => await client.assertCredential(params, tab);
const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
@@ -451,7 +454,7 @@ describe("FidoAuthenticatorService", () => {
domainSettingsService.neverDomains$ = of({ "bitwarden.com": null });
const result = async () => await client.assertCredential(params, tab);
const result = async () => await client.assertCredential(params, windowReference);
await expect(result).rejects.toThrow(FallbackRequestedError);
});
@@ -462,7 +465,7 @@ describe("FidoAuthenticatorService", () => {
rpId: "bitwarden.com",
});
const result = async () => await client.assertCredential(params, tab);
const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
@@ -477,7 +480,8 @@ describe("FidoAuthenticatorService", () => {
const abortController = new AbortController();
abortController.abort();
const result = async () => await client.assertCredential(params, tab, abortController);
const result = async () =>
await client.assertCredential(params, windowReference, abortController);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "AbortError" });
@@ -493,7 +497,7 @@ describe("FidoAuthenticatorService", () => {
new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.InvalidState),
);
const result = async () => await client.assertCredential(params, tab);
const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "InvalidStateError" });
@@ -505,7 +509,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams();
authenticator.getAssertion.mockRejectedValue(new Error("unknown error"));
const result = async () => await client.assertCredential(params, tab);
const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotAllowedError" });
@@ -516,7 +520,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams();
vaultSettingsService.enablePasskeys$ = of(false);
const result = async () => await client.assertCredential(params, tab);
const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
@@ -526,7 +530,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams();
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut);
const result = async () => await client.assertCredential(params, tab);
const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
@@ -535,7 +539,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw FallbackRequestedError if origin equals the bitwarden vault", async () => {
const params = createParams({ origin: VaultUrl });
const result = async () => await client.assertCredential(params, tab);
const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
@@ -555,7 +559,7 @@ describe("FidoAuthenticatorService", () => {
});
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
await client.assertCredential(params, tab);
await client.assertCredential(params, windowReference);
expect(authenticator.getAssertion).toHaveBeenCalledWith(
expect.objectContaining({
@@ -573,7 +577,7 @@ describe("FidoAuthenticatorService", () => {
}),
],
}),
tab,
windowReference,
expect.anything(),
);
});
@@ -585,7 +589,7 @@ describe("FidoAuthenticatorService", () => {
params.rpId = undefined;
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
await client.assertCredential(params, tab);
await client.assertCredential(params, windowReference);
});
});
@@ -597,7 +601,7 @@ describe("FidoAuthenticatorService", () => {
});
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
await client.assertCredential(params, tab);
await client.assertCredential(params, windowReference);
expect(authenticator.getAssertion).toHaveBeenCalledWith(
expect.objectContaining({
@@ -605,7 +609,7 @@ describe("FidoAuthenticatorService", () => {
rpId: RpId,
allowCredentialDescriptorList: [],
}),
tab,
windowReference,
expect.anything(),
);
});
@@ -627,7 +631,7 @@ describe("FidoAuthenticatorService", () => {
});
it("creates an active mediated conditional request", async () => {
await client.assertCredential(params, tab);
await client.assertCredential(params, windowReference);
expect(requestManager.newActiveRequest).toHaveBeenCalled();
expect(authenticator.getAssertion).toHaveBeenCalledWith(
@@ -635,14 +639,14 @@ describe("FidoAuthenticatorService", () => {
assumeUserPresence: true,
rpId: RpId,
}),
tab,
windowReference,
);
});
it("restarts the mediated conditional request if a user aborts the request", async () => {
authenticator.getAssertion.mockRejectedValueOnce(new Error());
await client.assertCredential(params, tab);
await client.assertCredential(params, windowReference);
expect(authenticator.getAssertion).toHaveBeenCalledTimes(2);
});
@@ -652,7 +656,7 @@ describe("FidoAuthenticatorService", () => {
abortController.abort();
authenticator.getAssertion.mockRejectedValueOnce(new DOMException("AbortError"));
await client.assertCredential(params, tab);
await client.assertCredential(params, windowReference);
expect(authenticator.getAssertion).toHaveBeenCalledTimes(2);
});

View File

@@ -47,7 +47,9 @@ import { guidToRawFormat } from "./guid-utils";
*
* It is highly recommended that the W3C specification is used a reference when reading this code.
*/
export class Fido2ClientService implements Fido2ClientServiceAbstraction {
export class Fido2ClientService<ParentWindowReference>
implements Fido2ClientServiceAbstraction<ParentWindowReference>
{
private timeoutAbortController: AbortController;
private readonly TIMEOUTS = {
NO_VERIFICATION: {
@@ -63,7 +65,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
};
constructor(
private authenticator: Fido2AuthenticatorService,
private authenticator: Fido2AuthenticatorService<ParentWindowReference>,
private configService: ConfigService,
private authService: AuthService,
private vaultSettingsService: VaultSettingsService,
@@ -102,7 +104,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
async createCredential(
params: CreateCredentialParams,
tab: chrome.tabs.Tab,
window: ParentWindowReference,
abortController = new AbortController(),
): Promise<CreateCredentialResult> {
const parsedOrigin = parse(params.origin, { allowPrivateDomains: true });
@@ -201,7 +203,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
try {
makeCredentialResult = await this.authenticator.makeCredential(
makeCredentialParams,
tab,
window,
abortController,
);
} catch (error) {
@@ -256,7 +258,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
async assertCredential(
params: AssertCredentialParams,
tab: chrome.tabs.Tab,
window: ParentWindowReference,
abortController = new AbortController(),
): Promise<AssertCredentialResult> {
const parsedOrigin = parse(params.origin, { allowPrivateDomains: true });
@@ -300,7 +302,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
if (params.mediation === "conditional") {
return this.handleMediatedConditionalRequest(
params,
tab,
window,
abortController,
clientDataJSONBytes,
);
@@ -324,7 +326,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
try {
getAssertionResult = await this.authenticator.getAssertion(
getAssertionParams,
tab,
window,
abortController,
);
} catch (error) {
@@ -363,7 +365,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
private async handleMediatedConditionalRequest(
params: AssertCredentialParams,
tab: chrome.tabs.Tab,
tab: ParentWindowReference,
abortController: AbortController,
clientDataJSONBytes: Uint8Array,
): Promise<AssertCredentialResult> {
@@ -379,7 +381,10 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
`[Fido2Client] started mediated request, available credentials: ${availableCredentials.length}`,
);
const requestResult = await this.requestManager.newActiveRequest(
tab.id,
// TODO: This isn't correct, but this.requestManager.newActiveRequest expects a number,
// while this class is currently generic over ParentWindowReference.
// Consider moving requestManager into browser and adding support for ParentWindowReference => tab.id
(tab as any).id,
availableCredentials,
abortController,
);

View File

@@ -7,7 +7,7 @@ import {
* Noop implementation of the {@link Fido2UserInterfaceService}.
* This implementation does not provide any user interface.
*/
export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction {
export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction<void> {
newSession(): Promise<Fido2UserInterfaceSession> {
throw new Error("Not implemented exception");
}