Skip to content

Commit

Permalink
Let user pick rp id instead of automatically selecting the first rp id.
Browse files Browse the repository at this point in the history
  • Loading branch information
sea-snake committed Feb 19, 2025
1 parent e963882 commit d218769
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 20 deletions.
59 changes: 57 additions & 2 deletions src/frontend/src/components/authenticateBox/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { i18n } from "$showcase/i18n";
import { mkAnchorInput } from "$src/components/anchorInput";
import { mkAnchorPicker } from "$src/components/anchorPicker";
import { flowErrorToastTemplate } from "$src/components/authenticateBox/errorToast";
import { displayError } from "$src/components/displayError";
import { arrowRight } from "$src/components/icons";
import { landingPage } from "$src/components/landingPage";
import { withLoader } from "$src/components/loader";
import { mainWindow } from "$src/components/mainWindow";
Expand All @@ -12,6 +14,7 @@ import {
reconstructPinIdentity,
} from "$src/crypto/pinIdentity";
import { registerTentativeDevice } from "$src/flows/addDevice/welcomeView/registerTentativeDevice";
import authorizeCopy from "$src/flows/authorize/index.json";
import { idbRetrievePinIdentityMaterial } from "$src/flows/pin/idb";
import { usePin } from "$src/flows/pin/usePin";
import { useRecovery } from "$src/flows/recovery/useRecovery";
Expand All @@ -31,6 +34,7 @@ import {
Connection,
InvalidAuthnMethod,
InvalidCaller,
LoginCancel,
LoginSuccess,
NoRegistrationFlow,
PinUserOtherDomain,
Expand Down Expand Up @@ -365,6 +369,7 @@ export const handleLoginFlowResult = async <E>(
| PossiblyWrongWebAuthnFlow
| PinUserOtherDomain
| FlowError
| LoginCancel
): Promise<
({ userNumber: bigint; connection: AuthenticatedConnection } & E) | undefined
> => {
Expand Down Expand Up @@ -400,6 +405,10 @@ export const handleLoginFlowResult = async <E>(
return undefined;
}

if (result.kind === "loginCancel") {
return undefined;
}

result satisfies FlowError;

toast.error(flowErrorToastTemplate(result));
Expand Down Expand Up @@ -625,7 +634,52 @@ const loginPasskey = ({
}: {
connection: Connection;
userNumber: bigint;
}) => connection.login(userNumber);
}) => connection.login(userNumber, pickRpId);

// Re-uses the anchor selector HTML structure and CSS classes
export type RpIdPickSuccess = {
kind: "rpIdPickSuccess";
rpId: string | undefined;
};
export type RpIdPickCancelled = { kind: "rpIdPickCancelled" };
export const pickRpId = (
rpIds: Array<string | undefined>
): Promise<RpIdPickSuccess | RpIdPickCancelled> =>
new Promise((resolve) => {
const copy = i18n.i18n(authorizeCopy);
const slot = html` <header>
<h1 class="t-title t-title--main">${copy.multiple_domains}</h1>
<p class="t-lead l-stack">${copy.multiple_domains_pick_to_continue}</p>
</header>
<ul class="c-list c-list--anchors l-stack">
${rpIds.map(
(rpId) =>
html` <li
class="c-list__item c-list__item--vip c-list__item--icon icon-trigger"
>
<button
class="c-list__parcel c-list__parcel--select"
@click="${() => resolve({ kind: "picked", rpId })}"

Check failure on line 662 in src/frontend/src/components/authenticateBox/index.ts

View workflow job for this annotation

GitHub Actions / frontend-checks

Type '"picked"' is not assignable to type '"rpIdPickSuccess"'.

Check failure on line 662 in src/frontend/src/components/authenticateBox/index.ts

View workflow job for this annotation

GitHub Actions / cached-build

Type '"picked"' is not assignable to type '"rpIdPickSuccess"'.

Check failure on line 662 in src/frontend/src/components/authenticateBox/index.ts

View workflow job for this annotation

GitHub Actions / clean-build (ubuntu-22.04)

Type '"picked"' is not assignable to type '"rpIdPickSuccess"'.
tabindex="0"
data-rp-id="${rpId}"
style="font-size: 18px"
>
${rpId ?? window.location.hostname}
</button>
<i class="c-list__icon c-list__icon--masked">${arrowRight}</i>
</li>`
)}
<li class="c-list__item c-list__item--noFocusStyle">
<button
class="t-link c-list__parcel c-list__parcel--fullwidth"
@click="${() => resolve({ kind: "rpIdPickCancelled" })}"
>
Cancel
</button>
</li>
</ul>`;
page({ slot, useLandingPageTemplate: false });
});

const loginPinIdentityMaterial = ({
connection,
Expand Down Expand Up @@ -747,7 +801,8 @@ const useIdentityFlow = async <I>({
);

const doLoginPasskey = async () => {
const result = await withLoader(() => loginPasskey(userNumber));
// const result = await withLoader(() => loginPasskey(userNumber));
const result = await loginPasskey(userNumber);
return { newAnchor: false, authnMethod: "passkey", ...result } as const;
};

Expand Down
4 changes: 3 additions & 1 deletion src/frontend/src/flows/authorize/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
"pick_subtitle": "to connect",
"pick_subtitle_join": "to",
"pick_alternative_of": "Choose the Internet Identity that connects to",
"pick_alternative_of_join": "and"
"pick_alternative_of_join": "and",
"multiple_domains": "Multiple domains",
"multiple_domains_pick_to_continue": "Multiple domains found, pick one to continue"
}
}
13 changes: 12 additions & 1 deletion src/frontend/src/flows/recovery/recoverWith/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { convertToValidCredentialData } from "$src/utils/credential-devices";
import {
AuthFail,
Connection,
LoginCancel,
LoginSuccess,
PossiblyWrongWebAuthnFlow,
WebAuthnFailed,
Expand Down Expand Up @@ -53,6 +54,11 @@ export const recoverWithDevice = ({
return;
}

if (result.kind === "loginCancel") {
resolve({ tag: "canceled" });
return;
}

if (result.kind !== "loginSuccess") {
if (result.kind === "possiblyWrongWebAuthnFlow") {
const i18n = new I18n();
Expand Down Expand Up @@ -90,6 +96,7 @@ const attemptRecovery = async ({
| WebAuthnFailed
| PossiblyWrongWebAuthnFlow
| AuthFail
| LoginCancel
| { kind: "noRecovery" }
| { kind: "tooManyRecovery" }
> => {
Expand All @@ -112,5 +119,9 @@ const attemptRecovery = async ({
.map(convertToValidCredentialData)
.filter(nonNullish);

return await connection.fromWebauthnCredentials(userNumber, credentialData);
return await connection.fromWebauthnCredentials(
userNumber,
credentialData,
undefined
);
};
6 changes: 6 additions & 0 deletions src/frontend/src/test-e2e/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,12 @@ export class AuthenticateView extends View {
await this.browser.$('[data-role="anchor-input"]').waitForDisplayed();
await this.browser.$('[data-role="anchor-input"]').setValue(anchor);
await this.browser.$('[data-action="continue"]').click();

// If there are multiple RP ids, always pick the first one
const selectRpId = await this.browser.$("[data-rp-id]");
if (await selectRpId.isExisting()) {
await selectRpId.click();
}
}

async expectAnchorInputField(): Promise<void> {
Expand Down
80 changes: 64 additions & 16 deletions src/frontend/src/utils/iiConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ import {
UserNumber,
VerifyTentativeDeviceResponse,
} from "$generated/internet_identity_types";
import {
RpIdPickCancelled,
RpIdPickSuccess,
} from "$src/components/authenticateBox";
import { fromMnemonicWithoutValidation } from "$src/crypto/ed25519";
import { DOMAIN_COMPATIBILITY, HARDWARE_KEY_TEST } from "$src/featureFlags";
import { features } from "$src/features";
Expand Down Expand Up @@ -103,6 +107,10 @@ export type LoginSuccess = {
showAddCurrentDevice: boolean;
};

export type LoginCancel = {
kind: "loginCancel";
};

export type RegFlowNextStep =
| { step: "checkCaptcha"; captcha_png_base64: string }
| { step: "finish" };
Expand Down Expand Up @@ -369,7 +377,10 @@ export class Connection {
};

login = async (
userNumber: bigint
userNumber: bigint,
pickRpId?: (
rpIds: Array<string | undefined>
) => Promise<RpIdPickSuccess | RpIdPickCancelled>
): Promise<
| LoginSuccess
| AuthFail
Expand All @@ -378,6 +389,7 @@ export class Connection {
| PinUserOtherDomain
| UnknownUser
| ApiError
| LoginCancel
> => {
let devices: Omit<DeviceData, "alias">[];
try {
Expand Down Expand Up @@ -416,29 +428,65 @@ export class Connection {
userNumber,
webAuthnAuthenticators
.map(convertToValidCredentialData)
.filter(nonNullish)
.filter(nonNullish),
pickRpId
);
};

fromWebauthnCredentials = async (
userNumber: bigint,
credentials: CredentialData[]
credentials: CredentialData[],
pickRpId?: (
rpIds: Array<string | undefined>
) => Promise<RpIdPickSuccess | RpIdPickCancelled>
): Promise<
LoginSuccess | WebAuthnFailed | PossiblyWrongWebAuthnFlow | AuthFail
| LoginSuccess
| WebAuthnFailed
| PossiblyWrongWebAuthnFlow
| AuthFail
| LoginCancel
> => {
if (isNullish(this.webAuthFlows) && DOMAIN_COMPATIBILITY.isEnabled()) {
const flows = findWebAuthnFlows({
supportsRor: supportsWebauthRoR(window.navigator.userAgent),
devices: credentials,
currentOrigin: window.location.origin,
// Empty array is the same as no related origins.
relatedOrigins: this.canisterConfig.related_origins[0] ?? [],
});
this.webAuthFlows = {
flows,
currentIndex: 0,
};
if (DOMAIN_COMPATIBILITY.isEnabled()) {
// Create flows if not initialized yet
if (isNullish(this.webAuthFlows)) {
const flows = findWebAuthnFlows({
supportsRor: supportsWebauthRoR(window.navigator.userAgent),
devices: credentials,
currentOrigin: window.location.origin,
// Empty array is the same as no related origins.
relatedOrigins: this.canisterConfig.related_origins[0] ?? [],
});
this.webAuthFlows = {
flows,
currentIndex: 0,
};
}

// When there are multiple, let user pick the rp id from discovered flows
const uniqueRpIds = Array.from(
new Set(this.webAuthFlows.flows.map((flow) => flow.rpId))
);
if (
nonNullish(pickRpId) &&
this.webAuthFlows.currentIndex === 0 &&
uniqueRpIds.length > 0
) {
const result = await pickRpId(uniqueRpIds);
if (result.kind === "rpIdPickCancelled") {
return { kind: "loginCancel" };
}
void (result satisfies RpIdPickSuccess);

const pickedFlows = this.webAuthFlows.flows.filter(
(flow) => flow.rpId === result.rpId
);
const otherFlows = this.webAuthFlows.flows.filter(
(flow) => flow.rpId !== result.rpId
);
this.webAuthFlows.flows = [...pickedFlows, ...otherFlows];
}
}

const flowsLength = this.webAuthFlows?.flows.length ?? 0;

// Better understand which users make it (or don't) all the way.
Expand Down

0 comments on commit d218769

Please sign in to comment.