Skip to content

Commit

Permalink
Add device based on credentials' origin
Browse files Browse the repository at this point in the history
  • Loading branch information
lmuntaner committed Jan 20, 2025
1 parent 053e161 commit 640049e
Show file tree
Hide file tree
Showing 14 changed files with 268 additions and 78 deletions.
14 changes: 11 additions & 3 deletions src/frontend/src/flows/addDevice/addCurrentDevice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { infoScreenTemplate } from "$src/components/infoScreen";
import { I18n } from "$src/i18n";
import { getCredentialsOrigin } from "$src/utils/credential-devices";
import { AuthenticatedConnection } from "$src/utils/iiConnection";
import { renderPage } from "$src/utils/lit-html";
import { TemplateResult } from "lit-html";
Expand Down Expand Up @@ -43,10 +44,17 @@ export const addCurrentDeviceScreen = (
addCurrentDevicePage({
i18n: new I18n(),
add: async () => {
const existingDevices = await connection.lookupAuthenticators(
userNumber
const credentials = await connection.lookupAuthenticators(userNumber);
const originNewDevice = getCredentialsOrigin({
credentials,
userAgent: navigator.userAgent,
});
await addCurrentDevice(
userNumber,
connection,
credentials,
originNewDevice
);
await addCurrentDevice(userNumber, connection, existingDevices);
resolve();
},
skip: () => resolve(),
Expand Down
9 changes: 7 additions & 2 deletions src/frontend/src/flows/addDevice/manage/addCurrentDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
AuthenticatedConnection,
creationOptions,
} from "$src/utils/iiConnection";
import { readDeviceOrigin } from "$src/utils/readDeviceOrigin";
import {
displayCancelError,
displayDuplicateDeviceError,
Expand Down Expand Up @@ -41,14 +42,17 @@ const displayFailedToAddDevice = (error: Error) =>
export const addCurrentDevice = async (
userNumber: bigint,
connection: AuthenticatedConnection,
devices: Omit<DeviceData, "alias">[]
devices: Omit<DeviceData, "alias">[],
origin: string | undefined
): Promise<void> => {
// Kick-off fetching "ua-parser-js";
const uaParser = loadUAParser();
let newDevice: WebAuthnIdentity;
// RP ID must be without schema and port
const rpId = origin !== undefined ? new URL(origin).hostname : undefined;
try {
newDevice = await WebAuthnIdentity.create({
publicKey: creationOptions(devices),
publicKey: creationOptions(devices, undefined, rpId),
});
} catch (error: unknown) {
if (isWebAuthnDuplicateDevice(error)) {
Expand Down Expand Up @@ -78,6 +82,7 @@ export const addCurrentDevice = async (
{ authentication: null },
newDevice.getPublicKey().toDer(),
{ unprotected: null },
origin ?? readDeviceOrigin(),
newDevice.rawId
)
);
Expand Down
7 changes: 6 additions & 1 deletion src/frontend/src/flows/addDevice/manage/addDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ export const addDevice = async ({
// If the user wants to add a FIDO device then we can (should) exit registration mode
// (only used for adding extra browsers)
await withLoader(() => connection.exitDeviceRegistrationMode());
await addCurrentDevice(userNumber, connection, anchorInfo.devices);
await addCurrentDevice(
userNumber,
connection,
anchorInfo.devices,
origin
);
return;
} else if (result === "canceled") {
// If the user canceled, disable registration mode and return
Expand Down
102 changes: 56 additions & 46 deletions src/frontend/src/flows/recovery/recoveryWizard.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
AnchorCredentials,
CredentialId,
PublicKey,
WebAuthnCredential,
} from "$generated/internet_identity_types";
import { DeviceData } from "$generated/internet_identity_types";
import { PinIdentityMaterial } from "../pin/idb";
import { getDevicesStatus } from "./recoveryWizard";

Expand All @@ -15,52 +10,67 @@ const lessThanAWeekAgo = nowInMillis - 1;
const pinIdentityMaterial: PinIdentityMaterial =
{} as unknown as PinIdentityMaterial;

const noCredentials: AnchorCredentials = {
credentials: [],
recovery_credentials: [],
recovery_phrases: [],
const noCredentials: Omit<DeviceData, "alias">[] = [];

const recoveryPhrase: Omit<DeviceData, "alias"> = {
origin: [],
protection: { unprotected: null },
// eslint-disable-next-line
pubkey: undefined as any,
key_type: { seed_phrase: null },
purpose: { recovery: null },
credential_id: [],
metadata: [],
};

const device: WebAuthnCredential = {
pubkey: [] as PublicKey,
credential_id: [] as CredentialId,
const device: Omit<DeviceData, "alias"> = {
origin: [],
protection: { unprotected: null },
// eslint-disable-next-line
pubkey: undefined as any,
key_type: { unknown: null },
purpose: { authentication: null },
credential_id: [],
metadata: [],
};

const oneDeviceOnly: AnchorCredentials = {
credentials: [device],
recovery_credentials: [],
recovery_phrases: [],
const recoveryDevice: Omit<DeviceData, "alias"> = {
metadata: [],
origin: [],
protection: { protected: null },
pubkey: new Uint8Array(),
key_type: { cross_platform: null },
purpose: { recovery: null },
credential_id: [Uint8Array.from([0, 0, 0, 0, 0])],
};

const oneRecoveryDeviceOnly: AnchorCredentials = {
credentials: [],
recovery_credentials: [device],
recovery_phrases: [],
};

const oneDeviceAndPhrase: AnchorCredentials = {
credentials: [device],
recovery_credentials: [],
recovery_phrases: [[] as PublicKey],
};

const twoDevices: AnchorCredentials = {
credentials: [device, { ...device }],
recovery_credentials: [],
recovery_phrases: [[] as PublicKey],
};

const threeDevices: AnchorCredentials = {
credentials: [device, { ...device }, { ...device }],
recovery_credentials: [],
recovery_phrases: [[] as PublicKey],
};

const oneNormalOneRecovery: AnchorCredentials = {
credentials: [device],
recovery_credentials: [device],
recovery_phrases: [[] as PublicKey],
};
const oneDeviceOnly: Omit<DeviceData, "alias">[] = [device];

const oneRecoveryDeviceOnly: Omit<DeviceData, "alias">[] = [recoveryDevice];

const oneDeviceAndPhrase: Omit<DeviceData, "alias">[] = [
device,
recoveryPhrase,
];

const twoDevices: Omit<DeviceData, "alias">[] = [
device,
{ ...device },
recoveryPhrase,
];

const threeDevices: Omit<DeviceData, "alias">[] = [
device,
{ ...device },
{ ...device },
recoveryPhrase,
];

const oneNormalOneRecovery: Omit<DeviceData, "alias">[] = [
device,
recoveryDevice,
recoveryPhrase,
];

test("getDevicesStatus returns 'pin-only' for user with pin and has seen recovery longer than a week ago", () => {
expect(
Expand Down
26 changes: 19 additions & 7 deletions src/frontend/src/flows/recovery/recoveryWizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { TemplateResult } from "lit-html";

import { AuthenticatedConnection } from "$src/utils/iiConnection";

import { AnchorCredentials } from "$generated/internet_identity_types";
import { DeviceData } from "$generated/internet_identity_types";
import { infoScreenTemplate } from "$src/components/infoScreen";
import { IdentityMetadata } from "$src/repositories/identityMetadata";
import { getCredentialsOrigin } from "$src/utils/credential-devices";
import { isNullish } from "@dfinity/utils";
import { addDevice } from "../addDevice/manage/addDevice";
import {
Expand Down Expand Up @@ -167,7 +168,7 @@ export const addDeviceWarning = ({
* * User has only one device.
*
* @param params {Object}
* @param params.credentials {AnchorCredentials}
* @param params.credentials {Omit<DeviceData, "alias">[]}
* @param params.identityMetadata {IdentityMetadata | undefined}
* @param params.pinIdentityMaterial {PinIdentityMaterial | undefined}
* @param params.nowInMillis {number}
Expand All @@ -180,7 +181,7 @@ export const getDevicesStatus = ({
pinIdentityMaterial,
nowInMillis,
}: {
credentials: AnchorCredentials;
credentials: Omit<DeviceData, "alias">[];
identityMetadata: IdentityMetadata | undefined;
pinIdentityMaterial: PinIdentityMaterial | undefined;
nowInMillis: number;
Expand All @@ -193,8 +194,10 @@ export const getDevicesStatus = ({
const showWarningPageEnabled = isNullish(
identityMetadata?.doNotShowRecoveryPageRequestTimestampMillis
);
const totalDevicesCount =
credentials.credentials.length + credentials.recovery_credentials.length;
const totalDevicesCount = credentials.filter(
(c) => !("seed_phrase" in c.key_type)
).length;

if (
totalDevicesCount <= 1 &&
hasNotSeenRecoveryPageLastWeek &&
Expand All @@ -220,7 +223,7 @@ export const recoveryWizard = async (
const [credentials, identityMetadata, pinIdentityMaterial] = await withLoader(
() =>
Promise.all([
connection.lookupCredentials(userNumber),
connection.lookupAll(userNumber),
connection.getIdentityMetadata(),
idbRetrievePinIdentityMaterial({
userNumber,
Expand All @@ -236,6 +239,11 @@ export const recoveryWizard = async (
nowInMillis,
});

const originNewDevice = getCredentialsOrigin({
credentials,
userAgent: navigator.userAgent,
});

if (devicesStatus !== "no-warning") {
connection.updateIdentityMetadata({
recoveryPageShownTimestampMillis: nowInMillis,
Expand All @@ -244,7 +252,11 @@ export const recoveryWizard = async (
status: devicesStatus,
});
if (userChoice.action === "add-device") {
await addDevice({ userNumber, connection, origin: window.origin });
await addDevice({
userNumber,
connection,
origin: originNewDevice ?? window.origin,
});
}
if (userChoice.action === "do-not-remind") {
connection.updateIdentityMetadata({
Expand Down
5 changes: 4 additions & 1 deletion src/frontend/src/flows/recovery/setupRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
creationOptions,
IC_DERIVATION_PATH,
} from "$src/utils/iiConnection";
import { readDeviceOrigin } from "$src/utils/readDeviceOrigin";
import { unreachable, unreachableLax } from "$src/utils/utils";
import { DerEncodedPublicKey, SignIdentity } from "@dfinity/agent";
import { WebAuthnIdentity } from "@dfinity/identity";
Expand Down Expand Up @@ -64,6 +65,7 @@ export const setupKey = async ({
{ recovery: null },
recoverIdentity.getPublicKey().toDer(),
{ unprotected: null },
readDeviceOrigin(),
recoverIdentity.rawId
);
});
Expand All @@ -89,7 +91,8 @@ export const setupPhrase = async (
{ seed_phrase: null },
{ recovery: null },
pubkey,
{ unprotected: null }
{ unprotected: null },
readDeviceOrigin()
)
),
});
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/flows/recovery/useRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Connection,
LoginSuccess,
} from "$src/utils/iiConnection";
import { readDeviceOrigin } from "$src/utils/readDeviceOrigin";
import { unknownToString, unreachableLax } from "$src/utils/utils";
import { constructIdentity } from "$src/utils/webAuthn";
import {
Expand Down Expand Up @@ -160,6 +161,7 @@ const enrollAuthenticator = async ({
{ authentication: null },
newDevice.getPublicKey().toDer(),
{ unprotected: null },
readDeviceOrigin(),
newDevice.rawId
);
} catch (error: unknown) {
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/src/utils/authnMethodData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {
MetadataMapV2,
} from "$generated/internet_identity_types";
import { CredentialId } from "$src/utils/credential-devices";
import { readDeviceOrigin } from "$src/utils/iiConnection";
import { DerEncodedPublicKey } from "@dfinity/agent";
import { nonNullish } from "@dfinity/utils";
import { readDeviceOrigin } from "./readDeviceOrigin";

/**
* Helper to create a new PinIdentity authn method to be used in a registration flow.
Expand Down Expand Up @@ -77,7 +77,7 @@ const defaultSecuritySettings = (): AuthnMethodSecuritySettings => {

const addOriginToMetadata = (metadata: MetadataMapV2) => {
const origin = readDeviceOrigin();
if (origin.length !== 0) {
if (origin !== undefined) {
metadata.push(["origin", { String: origin[0] }]);
}
};
Loading

0 comments on commit 640049e

Please sign in to comment.