Skip to content

Commit

Permalink
Remember successful rpId per anchor
Browse files Browse the repository at this point in the history
  • Loading branch information
lmuntaner committed Jan 14, 2025
1 parent f773987 commit 256f183
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 12 deletions.
6 changes: 5 additions & 1 deletion src/frontend/src/components/authenticateBox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,11 @@ export const handleLoginFlowResult = async <E>(
({ userNumber: bigint; connection: AuthenticatedConnection } & E) | undefined
> => {
if (result.kind === "loginSuccess") {
await setAnchorUsed(result.userNumber);
const rpIdPair =
result.rpIdUsed !== undefined
? { rpId: result.rpIdUsed, origin: window.location.origin }
: undefined;
await setAnchorUsed(result.userNumber, rpIdPair);
return result;
}

Expand Down
6 changes: 5 additions & 1 deletion src/frontend/src/flows/addDevice/manage/addFIDODevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ export const addFIDODevice = async (
);
}

await setAnchorUsed(userNumber);
// TODO: Set to default rpId when implementing ID-30
await setAnchorUsed(userNumber, {
rpId: null,
origin: window.location.origin,
});
};

const unknownError = (): Error => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ export const registerTentativeDevice = async (
if (isWebAuthnDuplicateDevice(result)) {
// Given that this is a remote device where we get the result that authentication should work,
// let's help the user and fill in their anchor number.
await setAnchorUsed(userNumber);
// TODO: Set to default rpId when implementing ID-30
await setAnchorUsed(userNumber, {
rpId: null,
origin: window.location.origin,
});
await displayDuplicateDeviceError({ primaryButton: "Ok" });
} else if (isWebAuthnCancel(result)) {
await displayCancelError({ primaryButton: "Ok" });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,11 @@ const handlePollResult = async ({
result: "match" | "canceled" | typeof AsyncCountdown.timeout;
}): Promise<"ok" | "canceled"> => {
if (result === "match") {
await setAnchorUsed(userNumber);
// TODO: Set to default rpId when implementing ID-30
await setAnchorUsed(userNumber, {
rpId: null,
origin: window.location.origin,
});
return "ok";
} else if (result === AsyncCountdown.timeout) {
await displayError({
Expand Down
5 changes: 4 additions & 1 deletion src/frontend/src/flows/manage/deviceSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { withLoader } from "$src/components/loader";
import { recoverWithPhrase } from "$src/flows/recovery/recoverWith/phrase";
import { phraseWizard } from "$src/flows/recovery/setupRecovery";
import { I18n } from "$src/i18n";
import { cleanUpRpIdMapper } from "$src/storage";
import {
AuthenticatedConnection,
bufferEqual,
Expand All @@ -20,7 +21,6 @@ import {
} from "$src/utils/recoveryDevice";
import { unknownToString } from "$src/utils/utils";
import { DerEncodedPublicKey } from "@dfinity/agent";

import copyJson from "./deviceSettings.json";

/* Rename the device and return */
Expand Down Expand Up @@ -56,10 +56,12 @@ export const deleteDevice = async ({
connection,
device,
reload,
userNumber,
}: {
connection: AuthenticatedConnection;
device: DeviceData;
reload: () => void;
userNumber: bigint;
}) => {
const pubKey: DerEncodedPublicKey = new Uint8Array(device.pubkey)
.buffer as DerEncodedPublicKey;
Expand Down Expand Up @@ -92,6 +94,7 @@ export const deleteDevice = async ({

await withLoader(async () => {
await connection.remove(device.pubkey);
await cleanUpRpIdMapper(userNumber);
});

if (sameDevice) {
Expand Down
5 changes: 3 additions & 2 deletions src/frontend/src/flows/manage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,8 @@ export const readRecovery = ({
} else {
return {
recoveryKey: {
remove: () => deleteDevice({ connection, device, reload }),
remove: () =>
deleteDevice({ connection, device, reload, userNumber }),
},
};
}
Expand Down Expand Up @@ -552,7 +553,7 @@ export const devicesFromDevicesWithUsage = ({
remove:
hasSingleDevice && !hasOtherAuthMethods
? undefined
: () => deleteDevice({ connection, device, reload }),
: () => deleteDevice({ connection, device, reload, userNumber }),
};

if ("browser_storage_key" in device.key_type) {
Expand Down
6 changes: 5 additions & 1 deletion src/frontend/src/flows/register/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,11 @@ export const registerFlow = async ({
// Immediately commit (and await) the metadata, so that the identity is fully set up when the user sees the success page
// This way, dropping of at that point does not negatively impact UX with additional nagging.
await withLoader(() =>
Promise.all([result.connection.commitMetadata(), setAnchorUsed(userNumber)])
Promise.all([
result.connection.commitMetadata(),
// TODO: Set to default rpId when implementing ID-30
setAnchorUsed(userNumber, { rpId: null, origin: window.location.origin }),
])
);
await displayUserNumber({
userNumber,
Expand Down
79 changes: 76 additions & 3 deletions src/frontend/src/storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,30 @@ export const getAnchors = async (): Promise<bigint[]> => {
return anchors;
};

/** Set the specified anchor as used "just now" */
export const setAnchorUsed = async (userNumber: bigint) => {
export const cleanUpRpIdMapper = async (userNumber: bigint) => {
await withStorage((storage) => {
const ix = userNumber.toString();
const anchors = storage.anchors;
const oldAnchor = anchors[ix];

if (isNullish(oldAnchor)) {
return storage;
}

storage.anchors[ix] = {
...oldAnchor,
originRpIDMapper: {},
};

return storage;
});
};

/** Set the specified anchor as used "just now" along with which RP ID and in which II origin it was used */
export const setAnchorUsed = async (
userNumber: bigint,
rpIdOriginPair?: { origin: string; rpId: string | null }
) => {
await withStorage((storage) => {
const ix = userNumber.toString();

Expand All @@ -41,10 +63,18 @@ export const setAnchorUsed = async (userNumber: bigint) => {
knownPrincipals: [],
};
const oldAnchor = anchors[ix] ?? defaultAnchor;
const originRpIDMapper = oldAnchor.originRpIDMapper ?? {};
if (rpIdOriginPair !== undefined) {
originRpIDMapper[rpIdOriginPair.origin] = rpIdOriginPair.rpId;
}

// Here we try to be as non-destructive as possible and we keep potentially unknown
// fields
storage.anchors[ix] = { ...oldAnchor, lastUsedTimestamp: nowMillis() };
storage.anchors[ix] = {
...oldAnchor,
lastUsedTimestamp: nowMillis(),
originRpIDMapper,
};
return storage;
});
};
Expand Down Expand Up @@ -81,6 +111,39 @@ export const getAnchorByPrincipal = async ({
return;
};

/**
* Returns the last RP ID successfully used for the specific anchor in the specific ii origin.
*
* @param params
* @param params.userNumber The anchor number.
* @param params.origin The origin of the ii.
* @returns {string | null | undefined} The RP ID used for the specific anchor in the specific ii origin.
* - `string` is the RP ID to be used.
* - `null` means that the RP ID used was `undefined`.
* - `undefined` means that the RP ID used was not found.
*/
export const getAnchorRpId = async ({
userNumber,
origin,
}: {
userNumber: bigint;
origin: string;
}): Promise<string | undefined | null> => {
const storage = await readStorage();
const anchors = storage.anchors;

const anchorData = anchors[userNumber.toString()];
if (
isNullish(anchorData) ||
isNullish(anchorData.originRpIDMapper) ||
anchorData.originRpIDMapper[origin] === undefined
) {
return undefined;
}

return anchorData.originRpIDMapper[origin];
};

/** Look up an anchor by principal, if it is the last used for the given origin.
*/
export const getAnchorIfLastUsed = async ({
Expand Down Expand Up @@ -653,9 +716,19 @@ const PrincipalDataV4 = z.object({
lastUsedTimestamp: z.number(),
});

/**
* Mapper of which RP ID was used to get the credential for each origin.
*
* Record<ii_origin, rp_id>
* rp_id can be `null` if the RP ID used was `undefined`.
* It means that we can skip the RP ID calculation and we need to set it as `undeifined`.
*/
const originRpIDMapper = z.record(z.string().nullable());

const AnchorV4 = z.object({
/** Timestamp (mills since epoch) of when anchor was last used */
lastUsedTimestamp: z.number(),
originRpIDMapper: originRpIDMapper.optional(),

knownPrincipals: z.array(PrincipalDataV4),
});
Expand Down
26 changes: 25 additions & 1 deletion src/frontend/src/utils/iiConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
IdentityMetadata,
IdentityMetadataRepository,
} from "$src/repositories/identityMetadata";
import { getAnchorRpId } from "$src/storage";
import { JWT, MockOpenID, OpenIDCredential, Salt } from "$src/utils/mockOpenID";
import { diagnosticInfo, unknownToString } from "$src/utils/utils";
import {
Expand Down Expand Up @@ -99,6 +100,9 @@ export type LoginSuccess = {
connection: AuthenticatedConnection;
userNumber: bigint;
showAddCurrentDevice: boolean;
// `null` means that no RP ID was used.
// `undefined` means that the field doesn't apply.
rpIdUsed?: string | null;
};

export type RegFlowNextStep =
Expand Down Expand Up @@ -331,6 +335,8 @@ export class Connection {
),
userNumber,
showAddCurrentDevice: false,
// TODO: Change to the default RP ID when we implement ID-30
rpIdUsed: undefined,
};
}

Expand Down Expand Up @@ -409,6 +415,12 @@ export class Connection {
userNumber: bigint,
credentials: CredentialData[]
): Promise<LoginSuccess | WebAuthnFailed | PossiblyWrongRPID | AuthFail> => {
console.log("in da fromWebauthnCredentials");
const rpIdOverride = await getAnchorRpId({
userNumber,
origin: window.location.origin,
});
console.log("rpIdOverride", rpIdOverride);
const cancelledRpIds = this._cancelledRpIds.get(userNumber) ?? new Set();
const currentOrigin = window.location.origin;
const dynamicRPIdEnabled =
Expand All @@ -420,8 +432,14 @@ export class Connection {
currentOrigin
);
const rpId = dynamicRPIdEnabled
? findWebAuthnRpId(currentOrigin, filteredCredentials, relatedDomains())
? // If `rpIdOverride` is `null` it means that last successful RP ID was `undefined`.
rpIdOverride === null
? undefined
: typeof rpIdOverride === "string"
? rpIdOverride
: findWebAuthnRpId(currentOrigin, filteredCredentials, relatedDomains())
: undefined;
console.log("rpId", rpId);

/* 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
Expand Down Expand Up @@ -472,11 +490,16 @@ export class Connection {
actor
);

// If RP ID is enabled and it's not set, we want to set it as so.
// `undefined` means that the field doesn't apply.
const rpIdUsed = dynamicRPIdEnabled && rpId === undefined ? null : rpId;

return {
kind: "loginSuccess",
userNumber,
connection,
showAddCurrentDevice: cancelledRpIds.size > 0,
rpIdUsed,
};
};
fromIdentity = async (
Expand All @@ -498,6 +521,7 @@ export class Connection {
userNumber,
connection,
showAddCurrentDevice: false,
rpIdUsed: undefined,
};
};

Expand Down

0 comments on commit 256f183

Please sign in to comment.