Skip to content

Commit

Permalink
Check for Password Manager extensions when adding new devices (#2842)
Browse files Browse the repository at this point in the history
  • Loading branch information
lmuntaner authored Feb 7, 2025
1 parent 734c8d5 commit d3d11ec
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 12 deletions.
5 changes: 2 additions & 3 deletions src/frontend/src/flows/manage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions src/frontend/src/flows/recovery/recoveryWizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions src/frontend/src/flows/recovery/setupRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions src/frontend/src/flows/recovery/useRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
48 changes: 48 additions & 0 deletions src/frontend/src/utils/rorSupport.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
24 changes: 24 additions & 0 deletions src/frontend/src/utils/rorSupport.ts
Original file line number Diff line number Diff line change
@@ -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;
};

0 comments on commit d3d11ec

Please sign in to comment.