From d3d11ec3ada74ed919a5c60f091301b638b7ffae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7=20Muntaner?= Date: Fri, 7 Feb 2025 11:03:40 +0100 Subject: [PATCH] Check for Password Manager extensions when adding new devices (#2842) --- src/frontend/src/flows/manage/index.ts | 5 +- .../src/flows/recovery/recoveryWizard.ts | 5 +- .../src/flows/recovery/setupRecovery.ts | 5 +- .../src/flows/recovery/useRecovery.ts | 5 +- src/frontend/src/utils/rorSupport.test.ts | 48 +++++++++++++++++++ src/frontend/src/utils/rorSupport.ts | 24 ++++++++++ 6 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 src/frontend/src/utils/rorSupport.test.ts create mode 100644 src/frontend/src/utils/rorSupport.ts diff --git a/src/frontend/src/flows/manage/index.ts b/src/frontend/src/flows/manage/index.ts index 8f48b3adbe..5f19a80fa4 100644 --- a/src/frontend/src/flows/manage/index.ts +++ b/src/frontend/src/flows/manage/index.ts @@ -50,7 +50,7 @@ import { isRecoveryDevice, isRecoveryPhrase, } from "$src/utils/recoveryDevice"; -import { supportsWebauthRoR } from "$src/utils/userAgent"; +import { userSupportsWebauthRoR } from "$src/utils/rorSupport"; import { OmitParams, isCanisterError, @@ -383,8 +383,7 @@ export const displayManage = async ( const onAddDevice = async () => { const newDeviveOrigin = - supportsWebauthRoR(window.navigator.userAgent) && - DOMAIN_COMPATIBILITY.isEnabled() + userSupportsWebauthRoR() && DOMAIN_COMPATIBILITY.isEnabled() ? getCredentialsOrigin({ credentials: devices_, userAgent: navigator.userAgent, diff --git a/src/frontend/src/flows/recovery/recoveryWizard.ts b/src/frontend/src/flows/recovery/recoveryWizard.ts index c55595983d..3239cb0510 100644 --- a/src/frontend/src/flows/recovery/recoveryWizard.ts +++ b/src/frontend/src/flows/recovery/recoveryWizard.ts @@ -10,7 +10,7 @@ import { infoScreenTemplate } from "$src/components/infoScreen"; import { DOMAIN_COMPATIBILITY } from "$src/featureFlags"; import { IdentityMetadata } from "$src/repositories/identityMetadata"; import { getCredentialsOrigin } from "$src/utils/credential-devices"; -import { supportsWebauthRoR } from "$src/utils/userAgent"; +import { userSupportsWebauthRoR } from "$src/utils/rorSupport"; import { isNullish } from "@dfinity/utils"; import { addDevice } from "../addDevice/manage/addDevice"; import { @@ -241,8 +241,7 @@ export const recoveryWizard = async ( }); const originNewDevice = - supportsWebauthRoR(window.navigator.userAgent) && - DOMAIN_COMPATIBILITY.isEnabled() + userSupportsWebauthRoR() && DOMAIN_COMPATIBILITY.isEnabled() ? getCredentialsOrigin({ credentials, userAgent: navigator.userAgent, diff --git a/src/frontend/src/flows/recovery/setupRecovery.ts b/src/frontend/src/flows/recovery/setupRecovery.ts index 1a45063c1d..3e42bf29fa 100644 --- a/src/frontend/src/flows/recovery/setupRecovery.ts +++ b/src/frontend/src/flows/recovery/setupRecovery.ts @@ -9,7 +9,7 @@ import { creationOptions, IC_DERIVATION_PATH, } from "$src/utils/iiConnection"; -import { supportsWebauthRoR } from "$src/utils/userAgent"; +import { userSupportsWebauthRoR } from "$src/utils/rorSupport"; import { unreachable, unreachableLax } from "$src/utils/utils"; import { WebAuthnIdentity } from "$src/utils/webAuthnIdentity"; import { DerEncodedPublicKey, SignIdentity } from "@dfinity/agent"; @@ -34,8 +34,7 @@ export const setupKey = async ({ const devices = devices_ ?? (await connection.lookupAll(connection.userNumber)); const newDeviceOrigin = - supportsWebauthRoR(window.navigator.userAgent) && - DOMAIN_COMPATIBILITY.isEnabled() + userSupportsWebauthRoR() && DOMAIN_COMPATIBILITY.isEnabled() ? getCredentialsOrigin({ credentials: devices, userAgent: window.navigator.userAgent, diff --git a/src/frontend/src/flows/recovery/useRecovery.ts b/src/frontend/src/flows/recovery/useRecovery.ts index 39408c0c93..141f34eb02 100644 --- a/src/frontend/src/flows/recovery/useRecovery.ts +++ b/src/frontend/src/flows/recovery/useRecovery.ts @@ -11,7 +11,7 @@ import { IIWebAuthnIdentity, LoginSuccess, } from "$src/utils/iiConnection"; -import { supportsWebauthRoR } from "$src/utils/userAgent"; +import { userSupportsWebauthRoR } from "$src/utils/rorSupport"; import { unknownToString, unreachableLax } from "$src/utils/utils"; import { constructIdentity } from "$src/utils/webAuthn"; import { @@ -132,8 +132,7 @@ const enrollAuthenticator = async ({ const newDeviceData = await withLoader(async () => { const devices = (await connection.getAnchorInfo()).devices; const newDeviceOrigin = - supportsWebauthRoR(window.navigator.userAgent) && - DOMAIN_COMPATIBILITY.isEnabled() + userSupportsWebauthRoR() && DOMAIN_COMPATIBILITY.isEnabled() ? getCredentialsOrigin({ credentials: devices, userAgent: window.navigator.userAgent, diff --git a/src/frontend/src/utils/rorSupport.test.ts b/src/frontend/src/utils/rorSupport.test.ts new file mode 100644 index 0000000000..062cb0bf3b --- /dev/null +++ b/src/frontend/src/utils/rorSupport.test.ts @@ -0,0 +1,48 @@ +import { userSupportsWebauthRoR } from "./rorSupport"; + +describe("rorSupport", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("userSupportsWebauthRoR", () => { + it("should return true if the user agent supports Webauthn with Related Origin Requests and the credential.get function is not monkey patched", () => { + vi.stubGlobal("navigator", { + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", + credentials: { + get: { + toString: () => "function get() { [native code] }", + }, + }, + }); + expect(userSupportsWebauthRoR()).toBe(true); + }); + + it("should return false if the user agent does not support Webauthn with Related Origin Requests", () => { + vi.stubGlobal("navigator", { + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15", + credentials: { + get: { + toString: () => "function get() { [native code] }", + }, + }, + }); + expect(userSupportsWebauthRoR()).toBe(false); + }); + + it("should return false if the credential.get function is monkey patched", () => { + vi.stubGlobal("navigator", { + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", + credentials: { + get: { + toString: () => "function get() { [non-native code] }", + }, + }, + }); + expect(userSupportsWebauthRoR()).toBe(false); + }); + }); +}); diff --git a/src/frontend/src/utils/rorSupport.ts b/src/frontend/src/utils/rorSupport.ts new file mode 100644 index 0000000000..93f84e1477 --- /dev/null +++ b/src/frontend/src/utils/rorSupport.ts @@ -0,0 +1,24 @@ +import { supportsWebauthRoR } from "./userAgent"; + +const isNative = (fn: () => unknown) => /\[native code\]/.test(fn.toString()); + +/** + * Util to find out whether the current user's browser supports Related Origin Requests. + * + * There are two things to consider: + * - Does the browser and version support RoR? + * - Does the user have an installed password manager extension? + * - Some extensions monkey patch the `navigator.credentials.get` function, e.g. 1Password. + * - We can check for this with `toString` method. + * - Others proxy `navigator.credentials.get` to their own implementation, e.g. NordPass. + * - We can't check this. + * + * @returns {boolean} + */ +export const userSupportsWebauthRoR = (): boolean => { + const userAgentSuportsRoR = supportsWebauthRoR(navigator.userAgent); + const hasMonkeyPatchedCredentialGet = !isNative( + window.navigator.credentials.get + ); + return userAgentSuportsRoR && !hasMonkeyPatchedCredentialGet; +};