From fa9640545cd530a9ecb6db6d31f3b14690862cba Mon Sep 17 00:00:00 2001 From: sea-snake Date: Wed, 26 Feb 2025 15:07:43 +0100 Subject: [PATCH 1/5] Create WebAuthn flows based on device order (recently used). --- .../src/utils/findWebAuthnFlows.test.ts | 21 ++ src/frontend/src/utils/findWebAuthnFlows.ts | 67 ++--- .../src/utils/findWebAuthnRpId.test.ts | 283 ------------------ src/frontend/src/utils/findWebAuthnRpId.ts | 152 ---------- 4 files changed, 47 insertions(+), 476 deletions(-) delete mode 100644 src/frontend/src/utils/findWebAuthnRpId.test.ts delete mode 100644 src/frontend/src/utils/findWebAuthnRpId.ts diff --git a/src/frontend/src/utils/findWebAuthnFlows.test.ts b/src/frontend/src/utils/findWebAuthnFlows.test.ts index c8c799130a..30276fe4ad 100644 --- a/src/frontend/src/utils/findWebAuthnFlows.test.ts +++ b/src/frontend/src/utils/findWebAuthnFlows.test.ts @@ -128,4 +128,25 @@ describe("findWebAuthnFlows", () => { expect(result).toEqual([{ useIframe: false, rpId: undefined }]); }); + + it("should return flows in order of devices (recently used)", () => { + const result = findWebAuthnFlows({ + supportsRor: true, + devices: [ + createMockCredential(nonCurrentOrigin2), + createMockCredential(currentOrigin), + createMockCredential(currentOrigin), + createMockCredential(nonCurrentOrigin1), + createMockCredential(nonCurrentOrigin2), + ], + currentOrigin: currentOrigin, + relatedOrigins: [currentOrigin, nonCurrentOrigin1, nonCurrentOrigin2], + }); + + expect(result).toEqual([ + { useIframe: true, rpId: nonCurrentOrigin2RpId }, + { useIframe: false, rpId: undefined }, + { useIframe: true, rpId: nonCurrentOrigin1RpId }, + ]); + }); }); diff --git a/src/frontend/src/utils/findWebAuthnFlows.ts b/src/frontend/src/utils/findWebAuthnFlows.ts index 0b2e88584d..2bf9fc2c40 100644 --- a/src/frontend/src/utils/findWebAuthnFlows.ts +++ b/src/frontend/src/utils/findWebAuthnFlows.ts @@ -1,8 +1,5 @@ +import { isNullish, nonNullish } from "@dfinity/utils"; import { CredentialData } from "./credential-devices"; -import { - excludeCredentialsFromOrigins, - findWebAuthnRpId, -} from "./findWebAuthnRpId"; export type WebAuthnFlow = { useIframe: boolean; @@ -26,9 +23,7 @@ type Parameters = { * - Which RP ID to use. This is used for the iframe or for Related Origin Requests. * * Logic: - * - To calculate the RP ID, we use the `findWebAuthnRpId` function. - * - Calculate the RP ID first with all the credentials. - * - For the subsequent RP IDs, the credentials' origin that matches the previous RP ID will be excluded. + * - To calculate the RP IDs, we look for all RP IDs within the devices * - At the moment, we only use non-iframe if the RP ID matches the current origin. to avoid bad UX, if the RP ID doesn't match the current origin, the iframe will be used. * * @param {Parameters} params - The parameters to find the webauthn steps. @@ -39,45 +34,35 @@ export const findWebAuthnFlows = ({ currentOrigin, relatedOrigins, }: Parameters): WebAuthnFlow[] => { - const steps: WebAuthnFlow[] = []; - let filteredCredentials = [...devices]; - const rpIds = new Set(); + const currentRpId = new URL(currentOrigin).hostname; + const relatedRpIds = relatedOrigins.map( + (relatedOrigin) => new URL(relatedOrigin).hostname + ); - while (filteredCredentials.length > 0) { - const rpId = findWebAuthnRpId( - currentOrigin, - filteredCredentials, - relatedOrigins - ); + // The devices are expected to be ordered by recently used first + const deviceRpIds = [ + ...new Set( + devices + // Device origin to RP ID (hostname) + .map((device) => + nonNullish(device.origin) && device.origin !== currentOrigin + ? new URL(device.origin).hostname + : undefined + ) + // Filter out RP IDs that are not within `relatedRpIds` + .filter((rpId) => isNullish(rpId) || relatedRpIds.includes(rpId)) + ), + ]; - // EXCEPTION: At the moment, to avoid bad UX, if the RP ID doesn't match the current origin, the iframe will be used. - // This is because it's hard to find out whether a user's credentials come from a third party password manager or not. - // The iframe workaround works for all users. - const useIframe = - rpId !== undefined && rpId !== new URL(currentOrigin).hostname; + // Create steps from `deviceRpIds`, currently that's one step per RP ID + const steps: WebAuthnFlow[] = deviceRpIds.map((rpId) => ({ + rpId, + useIframe: nonNullish(rpId) && rpId !== currentRpId, + })); - const isRepeatedStep = steps.some( - (step) => step.rpId === rpId && step.useIframe === useIframe - ); - // Exit when the flow is the same to avoid infinite loops. - if (isRepeatedStep) { - break; - } - - steps.push({ useIframe, rpId }); - rpIds.add(rpId); - filteredCredentials = excludeCredentialsFromOrigins( - filteredCredentials, - rpIds, - currentOrigin - ); - } - - // One that doesn't use any new domain. - const defaultStep = { useIframe: false, rpId: undefined }; // If there are no steps, add a default step. if (steps.length === 0) { - steps.push(defaultStep); + steps.push({ useIframe: false, rpId: undefined }); } return steps; diff --git a/src/frontend/src/utils/findWebAuthnRpId.test.ts b/src/frontend/src/utils/findWebAuthnRpId.test.ts deleted file mode 100644 index a2a5a44704..0000000000 --- a/src/frontend/src/utils/findWebAuthnRpId.test.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { CredentialData } from "./credential-devices"; -import { - excludeCredentialsFromOrigins, - findWebAuthnRpId, -} from "./findWebAuthnRpId"; - -const BETA_DOMAINS = [ - "https://beta.identity.ic0.app", - "https://beta.identity.internetcomputer.org", - "https://fgte5-ciaaa-aaaad-aaatq-cai.ic0.app", -]; - -const PROD_DOMAINS = [ - "https://identity.ic0.app", - "https://identity.internetcomputer.org", - "https://identity.icp0.io", -]; - -describe("findWebAuthnRpId", () => { - const mockDeviceData = (origin?: string): CredentialData => ({ - origin, - credentialId: new ArrayBuffer(1), - pubkey: new ArrayBuffer(1), - }); - - test("returns undefined if a device is registered for the current domain", () => { - const devices: CredentialData[] = [ - mockDeviceData("https://identity.ic0.app"), - mockDeviceData("https://identity.internetcomputer.org"), - mockDeviceData("https://identity.icp0.io"), - ]; - const currentUrl = "https://identity.ic0.app"; - - expect(findWebAuthnRpId(currentUrl, devices, PROD_DOMAINS)).toBeUndefined(); - }); - - test("returns undefined for devices with default domain when the current domain matches", () => { - const devices: CredentialData[] = [ - mockDeviceData(), // Empty origin defaults to defaultDomain `https://identity.ic0.ap` - mockDeviceData("https://identity.internetcomputer.org"), - mockDeviceData("https://identity.icp0.io"), - ]; - const currentUrl = "https://identity.ic0.app"; - - expect(findWebAuthnRpId(currentUrl, devices, PROD_DOMAINS)).toBeUndefined(); - }); - - test("returns undefined if a device is registered for the current domain for beta domains", () => { - const devices: CredentialData[] = [ - mockDeviceData("https://beta.identity.ic0.app"), - mockDeviceData("https://beta.identity.internetcomputer.org"), - ]; - const currentUrl = "https://beta.identity.ic0.app"; - - expect(findWebAuthnRpId(currentUrl, devices, BETA_DOMAINS)).toBeUndefined(); - }); - - test("returns undefined if a device is registered for the current domain", () => { - const devices: CredentialData[] = [ - mockDeviceData("https://identity.ic0.app"), - mockDeviceData("https://identity.internetcomputer.org"), - mockDeviceData("https://identity.icp0.io"), - ]; - const currentUrl = "https://identity.internetcomputer.org"; - - expect(findWebAuthnRpId(currentUrl, devices, PROD_DOMAINS)).toBeUndefined(); - }); - - test("returns undefined if a device is registered for the current domain", () => { - const devices: CredentialData[] = [ - mockDeviceData("https://beta.identity.ic0.app"), - mockDeviceData("https://fgte5-ciaaa-aaaad-aaatq-cai.ic0.app"), - ]; - const currentUrl = "https://fgte5-ciaaa-aaaad-aaatq-cai.ic0.app"; - - expect(findWebAuthnRpId(currentUrl, devices, BETA_DOMAINS)).toBeUndefined(); - }); - - test("returns the second default preferred domain if no device is registered for the current domain", () => { - const devices: CredentialData[] = [ - mockDeviceData("https://identity.internetcomputer.org"), - mockDeviceData("https://identity.icp0.io"), - ]; - const currentUrl = "https://identity.ic0.app"; - - expect(findWebAuthnRpId(currentUrl, devices, PROD_DOMAINS)).toBe( - "identity.internetcomputer.org" - ); - }); - - test("returns last beta if a device is registered for the current domain", () => { - const devices: CredentialData[] = [ - mockDeviceData("https://fgte5-ciaaa-aaaad-aaatq-cai.ic0.app"), - ]; - const currentUrl = "https://beta.identity.ic0.app"; - - expect(findWebAuthnRpId(currentUrl, devices, BETA_DOMAINS)).toBe( - "fgte5-ciaaa-aaaad-aaatq-cai.ic0.app" - ); - }); - - test("returns the first default preferred domain if no device is registered for the current domain", () => { - const devices: CredentialData[] = [ - mockDeviceData("https://identity.ic0.app"), - mockDeviceData("https://identity.icp0.io"), - ]; - const currentUrl = "https://identity.internetcomputer.org"; - - expect(findWebAuthnRpId(currentUrl, devices, PROD_DOMAINS)).toBe( - "identity.ic0.app" - ); - }); - - test("returns undefined if no related origins", () => { - const devices: CredentialData[] = [ - mockDeviceData("https://identity.ic0.app"), - mockDeviceData("https://identity.icp0.io"), - ]; - const currentUrl = "https://identity.internetcomputer.org"; - - expect(findWebAuthnRpId(currentUrl, devices, [])).toBe(undefined); - }); - - test("returns the least preferred domain if devices are only on that domain", () => { - const devices: CredentialData[] = [ - mockDeviceData("https://identity.icp0.io"), - ]; - const currentUrl = "https://identity.ic0.app"; - - expect(findWebAuthnRpId(currentUrl, devices, PROD_DOMAINS)).toBe( - "identity.icp0.io" - ); - }); - - test("uses preferred domains when provided", () => { - // Switch the order of the domains, internetcomputer.org is moved to last. - const switchedDomains = [ - "https://identity.ic0.app", - "https://identity.icp0.io", - "https://identity.internetcomputer.org", - ]; - - const devices: CredentialData[] = [ - mockDeviceData("https://identity.internetcomputer.org"), - mockDeviceData("https://identity.icp0.io"), - ]; - const currentUrl = "https://identity.ic0.app"; - - expect(findWebAuthnRpId(currentUrl, devices, switchedDomains)).toBe( - "identity.icp0.io" - ); - }); - - test("returns undefined if the current domain is invalid", () => { - const devices: CredentialData[] = [ - mockDeviceData("https://identity.ic0.app"), - ]; - const currentUrl = "not-a-valid-url"; - - expect(findWebAuthnRpId(currentUrl, devices, PROD_DOMAINS)).toBeUndefined(); - }); - - test("returns undefined if no devices are registered for the current or preferred domains", () => { - const devices: CredentialData[] = [ - mockDeviceData("https://otherdomain.com"), - ]; - const currentUrl = "https://identity.ic0.app"; - - expect(findWebAuthnRpId(currentUrl, devices, PROD_DOMAINS)).toBeUndefined(); - }); - - test("returns `undefined` if there are no registered devices", () => { - const devices: CredentialData[] = []; - const currentUrl = "https://identity.ic0.app"; - - expect(findWebAuthnRpId(currentUrl, devices, PROD_DOMAINS)).toBeUndefined(); - }); -}); - -describe("excludeCredentialsFromOrigins", () => { - const mockDeviceData = (origin?: string): CredentialData => ({ - origin, - credentialId: new ArrayBuffer(1), - pubkey: new ArrayBuffer(1), - }); - - test("excludes credentials from specified origins", () => { - const credentials = [ - mockDeviceData("https://identity.ic0.app"), - mockDeviceData("https://identity.internetcomputer.org"), - mockDeviceData("https://identity.icp0.io"), - ]; - const originsToExclude = new Set(["identity.ic0.app"]); - const currentOrigin = "https://identity.internetcomputer.org"; - - const result = excludeCredentialsFromOrigins( - credentials, - originsToExclude, - currentOrigin - ); - - expect(result).toHaveLength(2); - expect(result).toEqual([ - mockDeviceData("https://identity.internetcomputer.org"), - mockDeviceData("https://identity.icp0.io"), - ]); - }); - - test("treats undefined credential origins as DEFAULT_DOMAIN", () => { - const credentials = [ - mockDeviceData(undefined), // Should be treated as DEFAULT_DOMAIN - mockDeviceData("https://identity.internetcomputer.org"), - ]; - const originsToExclude = new Set(["identity.ic0.app"]); // Should match DEFAULT_DOMAIN - const currentOrigin = "https://identity.internetcomputer.org"; - - const result = excludeCredentialsFromOrigins( - credentials, - originsToExclude, - currentOrigin - ); - - expect(result).toHaveLength(1); - expect(result).toEqual([ - mockDeviceData("https://identity.internetcomputer.org"), - ]); - }); - - test("treats undefined origins in exclusion set as currentOrigin", () => { - const credentials = [ - mockDeviceData("https://identity.ic0.app"), - mockDeviceData("https://identity.internetcomputer.org"), - ]; - const originsToExclude = new Set([undefined]); // Should be treated as currentOrigin - const currentOrigin = "https://identity.internetcomputer.org"; - - const result = excludeCredentialsFromOrigins( - credentials, - originsToExclude, - currentOrigin - ); - - expect(result).toHaveLength(1); - expect(result).toEqual([mockDeviceData("https://identity.ic0.app")]); - }); - - test("returns empty array when all credentials are excluded", () => { - const credentials = [ - mockDeviceData("https://identity.ic0.app"), - mockDeviceData("https://identity.internetcomputer.org"), - ]; - const originsToExclude = new Set([ - "identity.ic0.app", - "identity.internetcomputer.org", - ]); - const currentOrigin = "https://identity.ic0.app"; - - const result = excludeCredentialsFromOrigins( - credentials, - originsToExclude, - currentOrigin - ); - - expect(result).toHaveLength(0); - }); - - test("returns all credentials when no origins to exclude", () => { - const credentials = [ - mockDeviceData("https://identity.ic0.app"), - mockDeviceData("https://identity.internetcomputer.org"), - ]; - const originsToExclude = new Set(); - const currentOrigin = "https://identity.ic0.app"; - - const result = excludeCredentialsFromOrigins( - credentials, - originsToExclude, - currentOrigin - ); - - expect(result).toEqual(credentials); - }); -}); diff --git a/src/frontend/src/utils/findWebAuthnRpId.ts b/src/frontend/src/utils/findWebAuthnRpId.ts deleted file mode 100644 index 385af7aa52..0000000000 --- a/src/frontend/src/utils/findWebAuthnRpId.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { II_LEGACY_ORIGIN } from "$src/constants"; -import { CredentialData } from "./credential-devices"; - -export const PROD_DOMAINS = [ - "https://identity.ic0.app", - "https://identity.internetcomputer.org", - "https://identity.icp0.io", -]; -export const BETA_DOMAINS = [ - "https://beta.identity.ic0.app", - "https://beta.identity.internetcomputer.org", - "https://fgte5-ciaaa-aaaad-aaatq-cai.ic0.app", -]; - -/** - * Returns the related domains ordered by preference. - * - * It reads the current URL and returns the set related to the current url. - */ -export const relatedDomains = (): string[] => { - const currentUrl = new URL(window.location.origin); - if (PROD_DOMAINS.includes(currentUrl.origin)) { - return PROD_DOMAINS; - } - if (BETA_DOMAINS.includes(currentUrl.origin)) { - return BETA_DOMAINS; - } - // Only beta and prod have related domains. - return []; -}; - -export const hasCredentialsFromMultipleOrigins = ( - credentials: CredentialData[] -): boolean => - new Set(credentials.map(({ origin }) => origin ?? II_LEGACY_ORIGIN)).size > 1; - -/** - * Filters out credentials from specific origins. - * - * This function takes a list of credentials and removes any that match the provided origins. - * If a credential has no origin (undefined), it is treated as if it had the `DEFAULT_DOMAIN`. - * Two origins match if they have the same hostname (domain). - * - * @param credentials - List of credential devices to filter - * @param rpIds - Set of origins to exclude (undefined values are treated as `currentOrigin`) - * @param currentOrigin - The current origin to use when comparing against undefined origins - * @returns Filtered list of credentials, excluding those from the specified origins - */ -export const excludeCredentialsFromOrigins = ( - credentials: CredentialData[], - rpIds: Set, - currentOrigin: string -): CredentialData[] => { - if (rpIds.size === 0) { - return credentials; - } - // Change `undefined` to the current origin. - const originsToExclude = Array.from(rpIds).map((origin) => - origin === undefined ? currentOrigin : `https://${origin}` - ); - return credentials.filter( - (credential) => - originsToExclude.filter((originToExclude) => - sameDomain(credential.origin ?? II_LEGACY_ORIGIN, originToExclude) - ).length === 0 - ); -}; - -const sameDomain = (url1: string, url2: string): boolean => - new URL(url1).hostname === new URL(url2).hostname; - -const hostname = (url: string): string => new URL(url).hostname; - -const getFirstHostname = (devices: CredentialData[]): string => { - if (devices[0] === undefined) { - throw new Error("Not possible. Call this function only if devices exist."); - } - return hostname(devices[0].origin ?? II_LEGACY_ORIGIN); -}; - -/** - * Helper to count devices for a domain, defaulting to "ic0.app" if origin is empty - * - * @param devices - The list of devices registered for the user. - * @param domain - The domain to check for devices. It must be a top and secondary level domain e.g. "ic0.app" - * We need this to support the beta domains with the same functions: beta.identity.ic0.app, beta.identity.internetcomputer.org - * @returns {DeviceData[]} The list of devices registered for the domain. - */ -const getDevicesForDomain = ( - devices: CredentialData[], - domain: string -): CredentialData[] => - devices.filter((d) => sameDomain(d.origin ?? II_LEGACY_ORIGIN, domain)); - -/** - * Returns the domain to use as the RP ID for WebAuthn registration. - * - * The algorithm is as follows: - * 1. If there is a device registered for the current domain, return undefined. - * 2. If there is no device registered for the current domain, check if there is a device registered for one of the preferred domains. - * If there is, return the first preferred domain that has a device registered. - * 3. If there is no device registered for the current domain and none of the preferred domains. - * Raise an error because the devices should be registered in one of the preferred domains. - * - * @param currentUrl - The current URL of the page. - * @param devices - The list of devices registered for the user. - * @param relatedDomains - Optional list of domains in order or preference to use as the RP ID. - * @returns {string | undefined} - * `string` The RP ID (as hostname without schema) to use for WebAuthn registration. - * `undefined` when the RP ID is the same as the current domain and is not needed. - * `undefined` when there are no devices registered for any of the preferred domains. - * `undefined` when there are no devices. - * `undefined` when the devices domain is not a valid URL. - * - * `undefined` means to continue with the default flow. - * In case of inconsistent state, let the default flow be. - */ -export const findWebAuthnRpId = ( - currentUrl: string, - devices: CredentialData[], - relatedDomains: string[] -): string | undefined => { - // If there are no related domains, RP ID should not be set. - if (relatedDomains.length === 0) { - return undefined; - } - if (devices.length === 0) { - return undefined; - } - - try { - // Try current domain first if devices exist - if (getDevicesForDomain(devices, currentUrl).length > 0) { - return undefined; - } - - // Check based on the order of preferred domains if there is no device with the current domain. - for (const domain of relatedDomains) { - const devicesForDomain = getDevicesForDomain(devices, domain); - if (devicesForDomain.length > 0) { - return getFirstHostname(devicesForDomain); - } - } - } catch (err: unknown) { - // This could happen if the devices domain is not a valid URL. - // In that case, let the default flow be. - console.error(err); - return undefined; - } - - return undefined; -}; From fbe13fbdaf764b46ce68abfd00f0e5e68c5a4f3d Mon Sep 17 00:00:00 2001 From: sea-snake Date: Wed, 26 Feb 2025 15:51:54 +0100 Subject: [PATCH 2/5] Return undefined if device origin is equal to current origin, else return RP ID (hostname) --- src/frontend/src/utils/findWebAuthnFlows.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/utils/findWebAuthnFlows.ts b/src/frontend/src/utils/findWebAuthnFlows.ts index 2bf9fc2c40..dcea0dd29f 100644 --- a/src/frontend/src/utils/findWebAuthnFlows.ts +++ b/src/frontend/src/utils/findWebAuthnFlows.ts @@ -1,3 +1,4 @@ +import { II_LEGACY_ORIGIN } from "$src/constants"; import { isNullish, nonNullish } from "@dfinity/utils"; import { CredentialData } from "./credential-devices"; @@ -45,8 +46,8 @@ export const findWebAuthnFlows = ({ devices // Device origin to RP ID (hostname) .map((device) => - nonNullish(device.origin) && device.origin !== currentOrigin - ? new URL(device.origin).hostname + device.origin !== currentOrigin + ? new URL(device.origin ?? II_LEGACY_ORIGIN).hostname : undefined ) // Filter out RP IDs that are not within `relatedRpIds` @@ -64,6 +65,5 @@ export const findWebAuthnFlows = ({ if (steps.length === 0) { steps.push({ useIframe: false, rpId: undefined }); } - return steps; }; From 2bd7e2168f8504d3c4a579abf19767dcd98b5570 Mon Sep 17 00:00:00 2001 From: sea-snake Date: Wed, 26 Feb 2025 16:09:23 +0100 Subject: [PATCH 3/5] Return undefined if device origin is equal to current origin, else return RP ID (hostname) --- src/frontend/src/utils/findWebAuthnFlows.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/utils/findWebAuthnFlows.ts b/src/frontend/src/utils/findWebAuthnFlows.ts index dcea0dd29f..066f3a0add 100644 --- a/src/frontend/src/utils/findWebAuthnFlows.ts +++ b/src/frontend/src/utils/findWebAuthnFlows.ts @@ -46,7 +46,7 @@ export const findWebAuthnFlows = ({ devices // Device origin to RP ID (hostname) .map((device) => - device.origin !== currentOrigin + device.origin !== currentOrigin && currentOrigin !== II_LEGACY_ORIGIN ? new URL(device.origin ?? II_LEGACY_ORIGIN).hostname : undefined ) From a87f2ac979c009ed596d1ed8a25b7a96281351fa Mon Sep 17 00:00:00 2001 From: sea-snake Date: Wed, 26 Feb 2025 16:16:55 +0100 Subject: [PATCH 4/5] Return undefined if device origin is equal to current origin, else return RP ID (hostname) --- src/frontend/src/utils/findWebAuthnFlows.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/utils/findWebAuthnFlows.ts b/src/frontend/src/utils/findWebAuthnFlows.ts index 066f3a0add..16d3998320 100644 --- a/src/frontend/src/utils/findWebAuthnFlows.ts +++ b/src/frontend/src/utils/findWebAuthnFlows.ts @@ -46,9 +46,10 @@ export const findWebAuthnFlows = ({ devices // Device origin to RP ID (hostname) .map((device) => - device.origin !== currentOrigin && currentOrigin !== II_LEGACY_ORIGIN - ? new URL(device.origin ?? II_LEGACY_ORIGIN).hostname - : undefined + device.origin === currentOrigin || + (currentOrigin === II_LEGACY_ORIGIN && isNullish(device.origin)) + ? undefined + : new URL(device.origin ?? II_LEGACY_ORIGIN).hostname ) // Filter out RP IDs that are not within `relatedRpIds` .filter((rpId) => isNullish(rpId) || relatedRpIds.includes(rpId)) From a0959c6c5f5c72345484e17c0170792aa12c2711 Mon Sep 17 00:00:00 2001 From: sea-snake Date: Wed, 26 Feb 2025 16:25:11 +0100 Subject: [PATCH 5/5] Return undefined if device origin is equal to current origin, else return RP ID (hostname) --- src/frontend/src/utils/findWebAuthnFlows.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/utils/findWebAuthnFlows.ts b/src/frontend/src/utils/findWebAuthnFlows.ts index 16d3998320..7e96b2a2f6 100644 --- a/src/frontend/src/utils/findWebAuthnFlows.ts +++ b/src/frontend/src/utils/findWebAuthnFlows.ts @@ -40,8 +40,8 @@ export const findWebAuthnFlows = ({ (relatedOrigin) => new URL(relatedOrigin).hostname ); - // The devices are expected to be ordered by recently used first - const deviceRpIds = [ + // The devices are expected to be ordered by recently used already + const orderedDeviceRpIds = [ ...new Set( devices // Device origin to RP ID (hostname) @@ -57,7 +57,7 @@ export const findWebAuthnFlows = ({ ]; // Create steps from `deviceRpIds`, currently that's one step per RP ID - const steps: WebAuthnFlow[] = deviceRpIds.map((rpId) => ({ + const steps: WebAuthnFlow[] = orderedDeviceRpIds.map((rpId) => ({ rpId, useIframe: nonNullish(rpId) && rpId !== currentRpId, }));