1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 12:40:26 +00:00

[PM-3812][PM-3809] Unify Create and Login Passkeys UI (#6403)

* PM-1235 Added component to display passkey on auth flow

* PM-1235 Implement basic structure and behaviour of UI

* PM-1235 Added localised strings

* PM-1235 Improved button UI

* Implemented view passkey button

* Implemented multiple matching passkeys

* Refactored fido2 popup to use browser popout windows service

* [PM-3807] feat: remove non-discoverable from fido2 user interface class

* [PM-3807] feat: merge fido2 component ui

* [PM-3807] feat: return `cipherId` from user interface

* [PM-3807] feat: merge credential creation logic in authenticator

* [PM-3807] feat: merge credential assertion logic in authenticator

* updated test cases and services using the config service

* [PM-3807] feat: add `discoverable` property to fido2keys

* [PM-3807] feat: assign discoverable property during creation

* [PM-3807] feat: save discoverable field to server

* [PM-3807] feat: filter credentials by rpId AND discoverable

* [PM-3807] chore: remove discoverable tests which are no longer needed

* [PM-3807] chore: remove all logic for handling standalone Fido2Key

View and components will be cleaned up as part of UI tickets

* [PM-3807] fix: add missing discoverable property handling to tests

* updated locales with new text

* Updated popout windows service to use defined type for custom width and height

* Update on unifying auth flow ui to align with architecture changes

* Moved click event

* Throw dom exception error if tab is null

* updated fido2key object to array

* removed discoverable key in client inerface service for now

* Get senderTabId from the query params and send to the view cipher component to allow the pop out close when the close button is clicked on the view cipher component

* Refactored view item if passkeys exists and the cipher row views by having an extra ng-conatiner for each case

* Allow fido2 pop out close wehn cancle is clicked on add edit component

* Removed makshift run in angular zone

* created focus directive to target first element in ngFor for displayed ciphers in fido2

* Refactored to use switch statement and added condtional on search and add div

* Adjusted footer link and added more features to the login flow

* Added host listener to abort when window is closed

* remove custom focus directive. instead stuck focus logic into fido2-cipher-row component

* Fixed bug where close and cancel on view and add component does not abort the fido2 request

* show info dialog when user account does not have master password

* Removed PopupUtilsService

* show info dialog when user account does not have master password

* Added comments

* Added comments

* made row height consistent

* update logo to be dynamic with theme selection

* added new translation key

* Dis some styling to align cipher items

* Changed passkey icon fill color

* updated flow of focus and selected items in the passkey popup

* Fixed bug when picking a credential

* Added text to lock popout screen

* Added passkeys test to home view

* changed class name

* Added uilocation as a query paramter to know if the user is in the popout window

* update fido2 component for dynamic subtitleText as well as additional appA11yTitle attrs

* moved another method out of html

* Added window id return to single action popout and used the window id to close and abort the popout

* removed duplicate activatedroute

* added a doNotSaveUrl true to 2fa options, so the previousUrl can remain as the fido2 url

* Added a div to restrict the use browser link ot the buttom left

* reverted view change which is handled by the view pr

* Updated locales text and removed unused variable

* Fixed issue where new cipher is not created for non discoverable keys

* switched from using svg for the logo to CL

* removed svg files

* default to browser implmentation if user is logged out of the browser exetension

* removed passkeys knowledge from login, 2fa

* Added fido2 use browser link component and a state service to reduce passkeys knowledge on the lock component

* removed function and removed unnecessary comment

* reverted to former

* [PM-4148] Added descriptive error messages (#6475)

* Added descriptive error messages

* Added descriptive error messages

* replaced fido2 state service with higher order inject functions

* removed null check for tab

* refactor fido2 cipher row component

* added a static abort function to the browser interface service

* removed width from content

* uncommented code

* removed sessionId from query params and redudant styles

* Put back removed sessionId

* Added fallbackRequested parameter to abortPopout and added comments to the standalone function

* minor styling update to fix padding and color on selected ciphers

* update padding again to address vertical pushdown of cipher selection

---------

Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: jng <jng@bitwarden.com>
This commit is contained in:
SmithThe4th
2023-10-10 16:34:54 -04:00
committed by GitHub
parent 94e5117c32
commit 68da3d9efd
28 changed files with 1105 additions and 311 deletions

View File

@@ -15,6 +15,7 @@ export abstract class Fido2AuthenticatorService {
**/
makeCredential: (
params: Fido2AuthenticatorMakeCredentialsParams,
tab: chrome.tabs.Tab,
abortController?: AbortController
) => Promise<Fido2AuthenticatorMakeCredentialResult>;
@@ -28,6 +29,7 @@ export abstract class Fido2AuthenticatorService {
*/
getAssertion: (
params: Fido2AuthenticatorGetAssertionParams,
tab: chrome.tabs.Tab,
abortController?: AbortController
) => Promise<Fido2AuthenticatorGetAssertionResult>;
}

View File

@@ -24,6 +24,7 @@ export abstract class Fido2ClientService {
*/
createCredential: (
params: CreateCredentialParams,
tab: chrome.tabs.Tab,
abortController?: AbortController
) => Promise<CreateCredentialResult>;
@@ -38,6 +39,7 @@ export abstract class Fido2ClientService {
*/
assertCredential: (
params: AssertCredentialParams,
tab: chrome.tabs.Tab,
abortController?: AbortController
) => Promise<AssertCredentialResult>;

View File

@@ -50,6 +50,7 @@ export abstract class Fido2UserInterfaceService {
*/
newSession: (
fallbackSupported: boolean,
tab: chrome.tabs.Tab,
abortController?: AbortController
) => Promise<Fido2UserInterfaceSession>;
}

View File

@@ -34,6 +34,7 @@ describe("FidoAuthenticatorService", () => {
let userInterfaceSession!: MockProxy<Fido2UserInterfaceSession>;
let syncService!: MockProxy<SyncService>;
let authenticator!: Fido2AuthenticatorService;
let tab!: chrome.tabs.Tab;
beforeEach(async () => {
cipherService = mock<CipherService>();
@@ -42,6 +43,7 @@ describe("FidoAuthenticatorService", () => {
userInterface.newSession.mockResolvedValue(userInterfaceSession);
syncService = mock<SyncService>();
authenticator = new Fido2AuthenticatorService(cipherService, userInterface, syncService);
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab;
});
describe("makeCredential", () => {
@@ -55,19 +57,19 @@ 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);
await authenticator.makeCredential(invalidParams.unsupportedAlgorithm, tab);
await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.NotSupported);
});
it("should throw error when requireResidentKey has invalid value", async () => {
const result = async () => await authenticator.makeCredential(invalidParams.invalidRk);
const result = async () => await authenticator.makeCredential(invalidParams.invalidRk, tab);
await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Unknown);
});
it("should throw error when requireUserVerification has invalid value", async () => {
const result = async () => await authenticator.makeCredential(invalidParams.invalidUv);
const result = async () => await authenticator.makeCredential(invalidParams.invalidUv, tab);
await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Unknown);
});
@@ -80,7 +82,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);
const result = async () => await authenticator.makeCredential(params, tab);
await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Constraint);
});
@@ -94,7 +96,7 @@ describe("FidoAuthenticatorService", () => {
for (const p of Object.values(invalidParams)) {
try {
await authenticator.makeCredential(p);
await authenticator.makeCredential(p, tab);
// eslint-disable-next-line no-empty
} catch {}
}
@@ -135,7 +137,7 @@ describe("FidoAuthenticatorService", () => {
userInterfaceSession.informExcludedCredential.mockResolvedValue();
try {
await authenticator.makeCredential(params);
await authenticator.makeCredential(params, tab);
// eslint-disable-next-line no-empty
} catch {}
@@ -146,7 +148,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw error", async () => {
userInterfaceSession.informExcludedCredential.mockResolvedValue();
const result = async () => await authenticator.makeCredential(params);
const result = async () => await authenticator.makeCredential(params, tab);
await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.NotAllowed);
});
@@ -157,7 +159,7 @@ describe("FidoAuthenticatorService", () => {
excludedCipher.organizationId = "someOrganizationId";
try {
await authenticator.makeCredential(params);
await authenticator.makeCredential(params, tab);
// eslint-disable-next-line no-empty
} catch {}
@@ -170,7 +172,7 @@ describe("FidoAuthenticatorService", () => {
for (const p of Object.values(invalidParams)) {
try {
await authenticator.makeCredential(p);
await authenticator.makeCredential(p, tab);
// eslint-disable-next-line no-empty
} catch {}
}
@@ -207,7 +209,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: userVerification,
});
await authenticator.makeCredential(params);
await authenticator.makeCredential(params, tab);
expect(userInterfaceSession.confirmNewCredential).toHaveBeenCalledWith({
credentialName: params.rpEntity.name,
@@ -225,7 +227,7 @@ describe("FidoAuthenticatorService", () => {
});
cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher);
await authenticator.makeCredential(params);
await authenticator.makeCredential(params, tab);
const saved = cipherService.encrypt.mock.lastCall?.[0];
expect(saved).toEqual(
@@ -262,7 +264,7 @@ describe("FidoAuthenticatorService", () => {
});
const params = await createParams();
const result = async () => await authenticator.makeCredential(params);
const result = async () => await authenticator.makeCredential(params, tab);
await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.NotAllowed);
});
@@ -277,7 +279,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);
const result = async () => await authenticator.makeCredential(params, tab);
await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Unknown);
});
@@ -318,7 +320,7 @@ describe("FidoAuthenticatorService", () => {
});
it("should return attestation object", async () => {
const result = await authenticator.makeCredential(params);
const result = await authenticator.makeCredential(params, tab);
const attestationObject = CBOR.decode(
Fido2Utils.bufferSourceToUint8Array(result.attestationObject).buffer
@@ -415,7 +417,7 @@ describe("FidoAuthenticatorService", () => {
describe("invalid input parameters", () => {
it("should throw error when requireUserVerification has invalid value", async () => {
const result = async () => await authenticator.getAssertion(invalidParams.invalidUv);
const result = async () => await authenticator.getAssertion(invalidParams.invalidUv, tab);
await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Unknown);
});
@@ -428,7 +430,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);
const result = async () => await authenticator.getAssertion(params, tab);
await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Constraint);
});
@@ -457,7 +459,7 @@ describe("FidoAuthenticatorService", () => {
userInterfaceSession.informCredentialNotFound.mockResolvedValue();
try {
await authenticator.getAssertion(params);
await authenticator.getAssertion(params, tab);
// eslint-disable-next-line no-empty
} catch {}
@@ -472,7 +474,7 @@ describe("FidoAuthenticatorService", () => {
userInterfaceSession.informCredentialNotFound.mockResolvedValue();
try {
await authenticator.getAssertion(params);
await authenticator.getAssertion(params, tab);
// eslint-disable-next-line no-empty
} catch {}
@@ -493,7 +495,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);
const result = async () => await authenticator.getAssertion(params, tab);
await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.NotAllowed);
});
@@ -532,7 +534,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: false,
});
await authenticator.getAssertion(params);
await authenticator.getAssertion(params, tab);
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: ciphers.map((c) => c.id),
@@ -548,7 +550,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: false,
});
await authenticator.getAssertion(params);
await authenticator.getAssertion(params, tab);
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: [discoverableCiphers[0].id],
@@ -565,7 +567,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: userVerification,
});
await authenticator.getAssertion(params);
await authenticator.getAssertion(params, tab);
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: ciphers.map((c) => c.id),
@@ -581,7 +583,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: false,
});
const result = async () => await authenticator.getAssertion(params);
const result = async () => await authenticator.getAssertion(params, tab);
await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.NotAllowed);
});
@@ -629,7 +631,7 @@ describe("FidoAuthenticatorService", () => {
const encrypted = Symbol();
cipherService.encrypt.mockResolvedValue(encrypted as any);
await authenticator.getAssertion(params);
await authenticator.getAssertion(params, tab);
expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted);
@@ -648,7 +650,7 @@ describe("FidoAuthenticatorService", () => {
});
it("should return an assertion result", async () => {
const result = await authenticator.getAssertion(params);
const result = await authenticator.getAssertion(params, tab);
const encAuthData = result.authenticatorData;
const rpIdHash = encAuthData.slice(0, 32);
@@ -689,7 +691,7 @@ describe("FidoAuthenticatorService", () => {
for (let i = 0; i < 10; ++i) {
await init(); // Reset inputs
const result = await authenticator.getAssertion(params);
const result = await authenticator.getAssertion(params, tab);
const counter = result.authenticatorData.slice(33, 37);
expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // double check that the counter doesn't change
@@ -706,7 +708,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);
const result = async () => await authenticator.getAssertion(params, tab);
await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Unknown);
});

View File

@@ -46,10 +46,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
async makeCredential(
params: Fido2AuthenticatorMakeCredentialsParams,
tab: chrome.tabs.Tab,
abortController?: AbortController
): Promise<Fido2AuthenticatorMakeCredentialResult> {
const userInterfaceSession = await this.userInterface.newSession(
params.fallbackSupported,
tab,
abortController
);
@@ -175,10 +177,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
async getAssertion(
params: Fido2AuthenticatorGetAssertionParams,
tab: chrome.tabs.Tab,
abortController?: AbortController
): Promise<Fido2AuthenticatorGetAssertionResult> {
const userInterfaceSession = await this.userInterface.newSession(
params.fallbackSupported,
tab,
abortController
);
try {

View File

@@ -1,5 +1,7 @@
import { mock, MockProxy } from "jest-mock-extended";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction";
import { Utils } from "../../../platform/misc/utils";
import {
@@ -24,13 +26,18 @@ const RpId = "bitwarden.com";
describe("FidoAuthenticatorService", () => {
let authenticator!: MockProxy<Fido2AuthenticatorService>;
let configService!: MockProxy<ConfigServiceAbstraction>;
let authService!: MockProxy<AuthService>;
let client!: Fido2ClientService;
let tab!: chrome.tabs.Tab;
beforeEach(async () => {
authenticator = mock<Fido2AuthenticatorService>();
configService = mock<ConfigServiceAbstraction>();
client = new Fido2ClientService(authenticator, configService);
authService = mock<AuthService>();
client = new Fido2ClientService(authenticator, configService, authService);
configService.getFeatureFlag.mockResolvedValue(true);
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab;
});
describe("createCredential", () => {
@@ -39,7 +46,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw error if sameOriginWithAncestors is false", async () => {
const params = createParams({ sameOriginWithAncestors: false });
const result = async () => await client.createCredential(params);
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotAllowedError" });
@@ -50,7 +57,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw error if user.id is too small", async () => {
const params = createParams({ user: { id: "", displayName: "name" } });
const result = async () => await client.createCredential(params);
const result = async () => await client.createCredential(params, tab);
await expect(result).rejects.toBeInstanceOf(TypeError);
});
@@ -64,7 +71,7 @@ describe("FidoAuthenticatorService", () => {
},
});
const result = async () => await client.createCredential(params);
const result = async () => await client.createCredential(params, tab);
await expect(result).rejects.toBeInstanceOf(TypeError);
});
@@ -79,7 +86,7 @@ describe("FidoAuthenticatorService", () => {
origin: "invalid-domain-name",
});
const result = async () => await client.createCredential(params);
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
@@ -93,7 +100,7 @@ describe("FidoAuthenticatorService", () => {
rp: { id: "bitwarden.com", name: "Bitwraden" },
});
const result = async () => await client.createCredential(params);
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
@@ -106,7 +113,7 @@ describe("FidoAuthenticatorService", () => {
rp: { id: "bitwarden.com", name: "Bitwraden" },
});
const result = async () => await client.createCredential(params);
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
@@ -122,7 +129,7 @@ describe("FidoAuthenticatorService", () => {
],
});
const result = async () => await client.createCredential(params);
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotSupportedError" });
@@ -137,7 +144,7 @@ describe("FidoAuthenticatorService", () => {
const abortController = new AbortController();
abortController.abort();
const result = async () => await client.createCredential(params, abortController);
const result = async () => await client.createCredential(params, tab, abortController);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "AbortError" });
@@ -152,7 +159,7 @@ describe("FidoAuthenticatorService", () => {
});
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
await client.createCredential(params);
await client.createCredential(params, tab);
expect(authenticator.makeCredential).toHaveBeenCalledWith(
expect.objectContaining({
@@ -165,6 +172,7 @@ describe("FidoAuthenticatorService", () => {
displayName: params.user.displayName,
}),
}),
tab,
expect.anything()
);
});
@@ -176,7 +184,7 @@ describe("FidoAuthenticatorService", () => {
new Fido2AutenticatorError(Fido2AutenticatorErrorCode.InvalidState)
);
const result = async () => await client.createCredential(params);
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "InvalidStateError" });
@@ -188,7 +196,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams();
authenticator.makeCredential.mockRejectedValue(new Error("unknown error"));
const result = async () => await client.createCredential(params);
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotAllowedError" });
@@ -199,7 +207,17 @@ describe("FidoAuthenticatorService", () => {
const params = createParams();
configService.getFeatureFlag.mockResolvedValue(false);
const result = async () => await client.createCredential(params);
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
});
it("should throw FallbackRequestedError if user is logged out", async () => {
const params = createParams();
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut);
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
@@ -256,7 +274,7 @@ describe("FidoAuthenticatorService", () => {
origin: "invalid-domain-name",
});
const result = async () => await client.assertCredential(params);
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
@@ -270,7 +288,7 @@ describe("FidoAuthenticatorService", () => {
rpId: "bitwarden.com",
});
const result = async () => await client.assertCredential(params);
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
@@ -283,7 +301,7 @@ describe("FidoAuthenticatorService", () => {
rpId: "bitwarden.com",
});
const result = async () => await client.assertCredential(params);
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
@@ -298,7 +316,7 @@ describe("FidoAuthenticatorService", () => {
const abortController = new AbortController();
abortController.abort();
const result = async () => await client.assertCredential(params, abortController);
const result = async () => await client.assertCredential(params, tab, abortController);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "AbortError" });
@@ -314,7 +332,7 @@ describe("FidoAuthenticatorService", () => {
new Fido2AutenticatorError(Fido2AutenticatorErrorCode.InvalidState)
);
const result = async () => await client.assertCredential(params);
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "InvalidStateError" });
@@ -326,7 +344,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams();
authenticator.getAssertion.mockRejectedValue(new Error("unknown error"));
const result = async () => await client.assertCredential(params);
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotAllowedError" });
@@ -337,7 +355,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams();
configService.getFeatureFlag.mockResolvedValue(false);
const result = async () => await client.assertCredential(params);
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
@@ -348,12 +366,22 @@ describe("FidoAuthenticatorService", () => {
const params = createParams();
params.sameOriginWithAncestors = false; // Simulating the falsey value
const result = async () => await client.assertCredential(params);
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotAllowedError" });
await rejects.toBeInstanceOf(DOMException);
});
it("should throw FallbackRequestedError if user is logged out", async () => {
const params = createParams();
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut);
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
});
});
describe("assert non-discoverable credential", () => {
@@ -369,7 +397,7 @@ describe("FidoAuthenticatorService", () => {
});
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
await client.assertCredential(params);
await client.assertCredential(params, tab);
expect(authenticator.getAssertion).toHaveBeenCalledWith(
expect.objectContaining({
@@ -387,6 +415,7 @@ describe("FidoAuthenticatorService", () => {
}),
],
}),
tab,
expect.anything()
);
});
@@ -400,7 +429,7 @@ describe("FidoAuthenticatorService", () => {
});
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
await client.assertCredential(params);
await client.assertCredential(params, tab);
expect(authenticator.getAssertion).toHaveBeenCalledWith(
expect.objectContaining({
@@ -408,6 +437,7 @@ describe("FidoAuthenticatorService", () => {
rpId: RpId,
allowCredentialDescriptorList: [],
}),
tab,
expect.anything()
);
});

View File

@@ -1,5 +1,7 @@
import { parse } from "tldts";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { FeatureFlag } from "../../../enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction";
import { LogService } from "../../../platform/abstractions/log.service";
@@ -37,6 +39,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
constructor(
private authenticator: Fido2AuthenticatorService,
private configService: ConfigServiceAbstraction,
private authService: AuthService,
private logService?: LogService
) {}
@@ -46,6 +49,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
async createCredential(
params: CreateCredentialParams,
tab: chrome.tabs.Tab,
abortController = new AbortController()
): Promise<CreateCredentialResult> {
const enableFido2VaultCredentials = await this.isFido2FeatureEnabled();
@@ -55,6 +59,13 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
throw new FallbackRequestedError();
}
const authStatus = await this.authService.getAuthStatus();
if (authStatus === AuthenticationStatus.LoggedOut) {
this.logService?.warning(`[Fido2Client] Fido2VaultCredential is not enabled`);
throw new FallbackRequestedError();
}
if (!params.sameOriginWithAncestors) {
this.logService?.warning(
`[Fido2Client] Invalid 'sameOriginWithAncestors' value: ${params.sameOriginWithAncestors}`
@@ -126,7 +137,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
// Set timeout before invoking authenticator
if (abortController.signal.aborted) {
this.logService?.info(`[Fido2Client] Aborted with AbortController`);
throw new DOMException(undefined, "AbortError");
throw new DOMException("The operation either timed out or was not allowed.", "AbortError");
}
const timeout = setAbortTimeout(
abortController,
@@ -138,6 +149,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
try {
makeCredentialResult = await this.authenticator.makeCredential(
makeCredentialParams,
tab,
abortController
);
} catch (error) {
@@ -154,16 +166,19 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
error.errorCode === Fido2AutenticatorErrorCode.InvalidState
) {
this.logService?.warning(`[Fido2Client] Unknown error: ${error}`);
throw new DOMException(undefined, "InvalidStateError");
throw new DOMException("Unknown error occured.", "InvalidStateError");
}
this.logService?.info(`[Fido2Client] Aborted by user: ${error}`);
throw new DOMException(undefined, "NotAllowedError");
throw new DOMException(
"The operation either timed out or was not allowed.",
"NotAllowedError"
);
}
if (abortController.signal.aborted) {
this.logService?.info(`[Fido2Client] Aborted with AbortController`);
throw new DOMException(undefined, "AbortError");
throw new DOMException("The operation either timed out or was not allowed.", "AbortError");
}
clearTimeout(timeout);
@@ -179,6 +194,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
async assertCredential(
params: AssertCredentialParams,
tab: chrome.tabs.Tab,
abortController = new AbortController()
): Promise<AssertCredentialResult> {
const enableFido2VaultCredentials = await this.isFido2FeatureEnabled();
@@ -188,6 +204,13 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
throw new FallbackRequestedError();
}
const authStatus = await this.authService.getAuthStatus();
if (authStatus === AuthenticationStatus.LoggedOut) {
this.logService?.warning(`[Fido2Client] Fido2VaultCredential is not enabled`);
throw new FallbackRequestedError();
}
if (!params.sameOriginWithAncestors) {
this.logService?.warning(
`[Fido2Client] Invalid 'sameOriginWithAncestors' value: ${params.sameOriginWithAncestors}`
@@ -230,7 +253,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
if (abortController.signal.aborted) {
this.logService?.info(`[Fido2Client] Aborted with AbortController`);
throw new DOMException(undefined, "AbortError");
throw new DOMException("The operation either timed out or was not allowed.", "AbortError");
}
const timeout = setAbortTimeout(abortController, params.userVerification, params.timeout);
@@ -239,6 +262,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
try {
getAssertionResult = await this.authenticator.getAssertion(
getAssertionParams,
tab,
abortController
);
} catch (error) {
@@ -255,16 +279,19 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
error.errorCode === Fido2AutenticatorErrorCode.InvalidState
) {
this.logService?.warning(`[Fido2Client] Unknown error: ${error}`);
throw new DOMException(undefined, "InvalidStateError");
throw new DOMException("Unknown error occured.", "InvalidStateError");
}
this.logService?.info(`[Fido2Client] Aborted by user: ${error}`);
throw new DOMException(undefined, "NotAllowedError");
throw new DOMException(
"The operation either timed out or was not allowed.",
"NotAllowedError"
);
}
if (abortController.signal.aborted) {
this.logService?.info(`[Fido2Client] Aborted with AbortController`);
throw new DOMException(undefined, "AbortError");
throw new DOMException("The operation either timed out or was not allowed.", "AbortError");
}
clearTimeout(timeout);