Skip to content

Commit

Permalink
Share CredentialData param type of fromCredentials (#2742)
Browse files Browse the repository at this point in the history
* Change param type of fromCredentials

* Fix import
  • Loading branch information
lmuntaner authored Dec 11, 2024
1 parent d6d758f commit 680f8a0
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 81 deletions.
6 changes: 2 additions & 4 deletions src/frontend/src/flows/recovery/recoverWith/device.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CredentialId, DeviceData } from "$generated/internet_identity_types";
import { promptUserNumberTemplate } from "$src/components/promptUserNumber";
import { toast } from "$src/components/toast";
import { convertToCredentialData } from "$src/utils/credential-devices";
import {
AuthFail,
Connection,
Expand Down Expand Up @@ -96,10 +97,7 @@ const attemptRecovery = async ({

const credentialData = recoveryCredentials
.filter(hasCredentialId)
.map(({ credential_id, pubkey }) => ({
pubkey,
credential_id: credential_id[0],
}));
.map(convertToCredentialData);

return await connection.fromWebauthnCredentials(userNumber, credentialData);
};
2 changes: 1 addition & 1 deletion src/frontend/src/utils/authnMethodData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
AuthnMethodSecuritySettings,
MetadataMapV2,
} from "$generated/internet_identity_types";
import { CredentialId } from "$src/utils/credential-devices";
import { readDeviceOrigin } from "$src/utils/iiConnection";
import { CredentialId } from "$src/utils/multiWebAuthnIdentity";
import { DerEncodedPublicKey } from "@dfinity/agent";
import { nonNullish } from "@dfinity/utils";

Expand Down
20 changes: 20 additions & 0 deletions src/frontend/src/utils/credential-devices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { DeviceData, DeviceKey } from "$generated/internet_identity_types";
import { DerEncodedPublicKey } from "@dfinity/agent";

export type CredentialId = ArrayBuffer;
export type CredentialData = {
pubkey: DerEncodedPublicKey;
credentialId: CredentialId;
origin?: string;
};

const derFromPubkey = (pubkey: DeviceKey): DerEncodedPublicKey =>
new Uint8Array(pubkey).buffer as DerEncodedPublicKey;

export const convertToCredentialData = (
device: Omit<DeviceData, "alias">
): CredentialData => ({
credentialId: Buffer.from(device.credential_id[0] ?? []),
pubkey: derFromPubkey(device.pubkey),
origin: device.origin[0],
});
75 changes: 36 additions & 39 deletions src/frontend/src/utils/findWebAuthnRpId.test.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,64 @@
import { DeviceData } from "$generated/internet_identity_types";
import { CredentialData } from "./credential-devices";
import { findWebAuthnRpId } from "./findWebAuthnRpId";

describe("findWebAuthnRpId", () => {
const mockDeviceData = (origin: [] | [string]): DeviceData => ({
const mockDeviceData = (origin?: string): CredentialData => ({
origin,
alias: "test-device",
metadata: [],
protection: { protected: null },
pubkey: [],
key_type: { platform: null },
purpose: { authentication: null },
credential_id: [],
credentialId: new ArrayBuffer(1),
pubkey: new ArrayBuffer(1),
});

beforeEach(() => {
vi.spyOn(console, "error").mockImplementation(() => {});
});

test("returns undefined if a device is registered for the current domain", () => {
const devices: DeviceData[] = [
mockDeviceData(["https://identity.ic0.app"]),
mockDeviceData(["https://identity.internetcomputer.org"]),
mockDeviceData(["https://identity.icp0.io"]),
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)).toBeUndefined();
});

test("returns undefined for devices with default domain when the current domain matches", () => {
const devices: DeviceData[] = [
mockDeviceData([]), // Empty origin defaults to defaultDomain `https://identity.ic0.app`
mockDeviceData(["https://identity.internetcomputer.org"]),
mockDeviceData(["https://identity.icp0.io"]),
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)).toBeUndefined();
});

test("returns undefined if a device is registered for the current domain", () => {
const devices: DeviceData[] = [
mockDeviceData(["https://beta.identity.ic0.app"]),
mockDeviceData(["https://beta.identity.internetcomputer.org"]),
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)).toBeUndefined();
});

test("returns undefined if a device is registered for the current domain", () => {
const devices: DeviceData[] = [
mockDeviceData(["https://identity.ic0.app"]),
mockDeviceData(["https://identity.internetcomputer.org"]),
mockDeviceData(["https://identity.icp0.io"]),
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)).toBeUndefined();
});

test("returns the second default preferred domain if no device is registered for the current domain", () => {
const devices: DeviceData[] = [
mockDeviceData(["https://identity.internetcomputer.org"]),
mockDeviceData(["https://identity.icp0.io"]),
const devices: CredentialData[] = [
mockDeviceData("https://identity.internetcomputer.org"),
mockDeviceData("https://identity.icp0.io"),
];
const currentUrl = "https://identity.ic0.app";

Expand All @@ -73,9 +68,9 @@ describe("findWebAuthnRpId", () => {
});

test("returns the first default preferred domain if no device is registered for the current domain", () => {
const devices: DeviceData[] = [
mockDeviceData(["https://identity.ic0.app"]),
mockDeviceData(["https://identity.icp0.io"]),
const devices: CredentialData[] = [
mockDeviceData("https://identity.ic0.app"),
mockDeviceData("https://identity.icp0.io"),
];
const currentUrl = "https://identity.internetcomputer.org";

Expand All @@ -85,8 +80,8 @@ describe("findWebAuthnRpId", () => {
});

test("returns the least preferred domain if devices are only on that domain", () => {
const devices: DeviceData[] = [
mockDeviceData(["https://identity.icp0.io"]),
const devices: CredentialData[] = [
mockDeviceData("https://identity.icp0.io"),
];
const currentUrl = "https://identity.ic0.app";

Expand All @@ -98,9 +93,9 @@ describe("findWebAuthnRpId", () => {
test("uses preferred domains when provided", () => {
const preferredDomains = ["ic0.app", "icp0.io", "internetcomputer.org"];

const devices: DeviceData[] = [
mockDeviceData(["https://identity.internetcomputer.org"]),
mockDeviceData(["https://identity.icp0.io"]),
const devices: CredentialData[] = [
mockDeviceData("https://identity.internetcomputer.org"),
mockDeviceData("https://identity.icp0.io"),
];
const currentUrl = "https://identity.ic0.app";

Expand All @@ -110,8 +105,8 @@ describe("findWebAuthnRpId", () => {
});

test("throws an error if the current domain is invalid", () => {
const devices: DeviceData[] = [
mockDeviceData(["https://identity.ic0.app"]),
const devices: CredentialData[] = [
mockDeviceData("https://identity.ic0.app"),
];
const currentUrl = "not-a-valid-url";

Expand All @@ -121,7 +116,9 @@ describe("findWebAuthnRpId", () => {
});

test("throws an error if no devices are registered for the current or preferred domains", () => {
const devices: DeviceData[] = [mockDeviceData(["https://otherdomain.com"])];
const devices: CredentialData[] = [
mockDeviceData("https://otherdomain.com"),
];
const currentUrl = "https://identity.ic0.app";

expect(() => findWebAuthnRpId(currentUrl, devices)).toThrowError(
Expand All @@ -130,7 +127,7 @@ describe("findWebAuthnRpId", () => {
});

test("throws an error if there are no registered devices", () => {
const devices: DeviceData[] = [];
const devices: CredentialData[] = [];
const currentUrl = "https://identity.ic0.app";

expect(() => findWebAuthnRpId(currentUrl, devices)).toThrowError(
Expand Down
20 changes: 9 additions & 11 deletions src/frontend/src/utils/findWebAuthnRpId.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DeviceData } from "$generated/internet_identity_types";
import { CredentialData } from "./credential-devices";

const DEFAULT_DOMAIN = "https://identity.ic0.app";

Expand Down Expand Up @@ -32,14 +32,12 @@ const getTopAndSecondaryLevelDomain = (url: string): string => {
* @returns {DeviceData[]} The list of devices registered for the domain.
*/
const getDevicesForDomain = (
devices: DeviceData[],
devices: CredentialData[],
domain: string
): DeviceData[] =>
devices.filter((d) => {
if (d.origin.length === 0)
return domain === getTopAndSecondaryLevelDomain(DEFAULT_DOMAIN);
return d.origin.some((o) => getTopAndSecondaryLevelDomain(o) === domain);
});
): CredentialData[] =>
devices.filter(
(d) => getTopAndSecondaryLevelDomain(d.origin ?? DEFAULT_DOMAIN) === domain
);

/**
* Returns the domain to use as the RP ID for WebAuthn registration.
Expand All @@ -63,7 +61,7 @@ const getDevicesForDomain = (
*/
export const findWebAuthnRpId = (
currentUrl: string,
devices: DeviceData[],
devices: CredentialData[],
preferredDomains: string[] = ["ic0.app", "internetcomputer.org", "icp0.io"]
): string | undefined => {
const currentDomain = getTopAndSecondaryLevelDomain(currentUrl);
Expand All @@ -74,13 +72,13 @@ export const findWebAuthnRpId = (
);
}

const getFirstDomain = (devices: DeviceData[]): string => {
const getFirstDomain = (devices: CredentialData[]): string => {
if (devices[0] === undefined) {
throw new Error(
"Not possible. Call this function only if devices exist."
);
}
return devices[0].origin[0] ?? DEFAULT_DOMAIN;
return devices[0].origin ?? DEFAULT_DOMAIN;
};

// Try current domain first if devices exist
Expand Down
17 changes: 4 additions & 13 deletions src/frontend/src/utils/iiConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
Timestamp,
UserNumber,
VerifyTentativeDeviceResponse,
WebAuthnCredential,
} from "$generated/internet_identity_types";
import { fromMnemonicWithoutValidation } from "$src/crypto/ed25519";
import { features } from "$src/features";
Expand All @@ -50,6 +49,7 @@ import {
} from "@dfinity/identity";
import { Principal } from "@dfinity/principal";
import { isNullish, nonNullish } from "@dfinity/utils";
import { convertToCredentialData, CredentialData } from "./credential-devices";
import { MultiWebAuthnIdentity } from "./multiWebAuthnIdentity";
import { isRecoveryDevice, RecoveryDevice } from "./recoveryDevice";
import { isWebAuthnCancel } from "./webAuthnErrorUtils";
Expand Down Expand Up @@ -361,30 +361,21 @@ export class Connection {

return this.fromWebauthnCredentials(
userNumber,
devices.flatMap(({ credential_id, pubkey }) => {
return credential_id.length === 0
? []
: [{ credential_id: credential_id[0], pubkey }];
})
devices.map(convertToCredentialData)
);
};

fromWebauthnCredentials = async (
userNumber: bigint,
credentials: WebAuthnCredential[]
credentials: CredentialData[]
): Promise<LoginSuccess | WebAuthnFailed | AuthFail> => {
/* Recover the Identity (i.e. key pair) used when creating the anchor.
* If the "DUMMY_AUTH" feature is set, we use a dummy identity, the same identity
* that is used in the register flow.
*/
const identity = features.DUMMY_AUTH
? new DummyIdentity()
: MultiWebAuthnIdentity.fromCredentials(
credentials.map(({ credential_id, pubkey }) => ({
pubkey: derFromPubkey(pubkey),
credentialId: Buffer.from(credential_id),
}))
);
: MultiWebAuthnIdentity.fromCredentials(credentials);
let delegationIdentity: DelegationIdentity;

// Here we expect a webauth exception if the user canceled the webauthn prompt (triggered by
Expand Down
14 changes: 2 additions & 12 deletions src/frontend/src/utils/multiWebAuthnIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,13 @@
* then we know which one the user is actually using
* - It doesn't support creating credentials; use `WebAuthnIdentity` for that
*/
import {
DerEncodedPublicKey,
PublicKey,
Signature,
SignIdentity,
} from "@dfinity/agent";
import { PublicKey, Signature, SignIdentity } from "@dfinity/agent";
import { DER_COSE_OID, unwrapDER, WebAuthnIdentity } from "@dfinity/identity";
import { isNullish } from "@dfinity/utils";
import borc from "borc";
import { CredentialData } from "./credential-devices";
import { bufferEqual } from "./iiConnection";

export type CredentialId = ArrayBuffer;
export type CredentialData = {
pubkey: DerEncodedPublicKey;
credentialId: CredentialId;
};

/**
* A SignIdentity that uses `navigator.credentials`. See https://webauthn.guide/ for
* more information about WebAuthentication.
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/utils/webAuthn.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { DeviceData } from "$generated/internet_identity_types";
import { features } from "$src/features";
import {
creationOptions,
DummyIdentity,
IIWebAuthnIdentity,
creationOptions,
} from "$src/utils/iiConnection";
import { diagnosticInfo, unknownToString } from "$src/utils/utils";
import { WebAuthnIdentity } from "@dfinity/identity";
Expand Down

0 comments on commit 680f8a0

Please sign in to comment.