Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable PIN registration with query param #2711

Merged
merged 6 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/frontend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ export const PORTAL_II_URL = "https://internetcomputer.org/internet-identity";
// Default support page URL for when error is shown to user
export const ERROR_SUPPORT_URL =
"https://identitysupport.dfinity.org/hc/en-us/articles/32301362727188";

// Pin is disallowed by default unless this query parameter is set.
// This is used for testing purposes because we still support logging in with PIN but not registering with it.
export const ENABLE_PIN_QUERY_PARAM_KEY = "enablePin";
1 change: 1 addition & 0 deletions src/frontend/src/flows/authorize/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ const authenticate = async (
dapp.hasOrigin(authContext.requestOrigin)
),
}),
// This allows logging in with a PIN but not registering with a PIN
allowPinAuthentication:
authContext.authRequest.allowPinAuthentication ?? true,
autoSelectionIdentity: autoSelectionIdentity,
Expand Down
9 changes: 6 additions & 3 deletions src/frontend/src/flows/manage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { withLoader } from "$src/components/loader";
import { logoutSection } from "$src/components/logout";
import { mainWindow } from "$src/components/mainWindow";
import { toast } from "$src/components/toast";
import { LEGACY_II_URL } from "$src/config";
import { ENABLE_PIN_QUERY_PARAM_KEY, LEGACY_II_URL } from "$src/config";
import { addDevice } from "$src/flows/addDevice/manage/addDevice";
import { dappsExplorer } from "$src/flows/dappsExplorer";
import { KnownDapp, getDapps } from "$src/flows/dappsExplorer/dapps";
Expand Down Expand Up @@ -96,6 +96,10 @@ export const authFlowManage = async (connection: Connection) => {
const i18n = new I18n();
const dapps = shuffleArray(getDapps());

const params = new URLSearchParams(window.location.search);
const allowPinAuthentication =
params.get(ENABLE_PIN_QUERY_PARAM_KEY) !== null;

const identityBackground = new PreLoadImage(identityCardBackground);
// Go through the login flow, potentially creating an anchor.
const {
Expand All @@ -106,8 +110,7 @@ export const authFlowManage = async (connection: Connection) => {
connection,
i18n,
templates: authnTemplateManage({ dapps }),
allowPinAuthentication:
true /* when authenticating to II directly we always allow pin */,
allowPinAuthentication,
});

// Here, if the user is returning & doesn't have any recovery device, we prompt them to add
Expand Down
116 changes: 96 additions & 20 deletions src/frontend/src/flows/register/index.ts
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in this PR are mainly reverting #2687

Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { AuthnMethodData } from "$generated/internet_identity_types";
import { withLoader } from "$src/components/loader";
import { PinIdentityMaterial } from "$src/crypto/pinIdentity";
import { ENABLE_PIN_QUERY_PARAM_KEY } from "$src/config";
import {
PinIdentityMaterial,
constructPinIdentity,
} from "$src/crypto/pinIdentity";
import { idbStorePinIdentityMaterial } from "$src/flows/pin/idb";
import { registerDisabled } from "$src/flows/registerDisabled";
import { I18n } from "$src/i18n";
import { setAnchorUsed } from "$src/storage";
import { passkeyAuthnMethodData } from "$src/utils/authnMethodData";
import {
passkeyAuthnMethodData,
pinAuthnMethodData,
} from "$src/utils/authnMethodData";
import {
AlreadyInProgress,
ApiError,
Expand All @@ -23,7 +31,10 @@ import {
import { SignIdentity } from "@dfinity/agent";
import { ECDSAKeyIdentity } from "@dfinity/identity";
import { nonNullish } from "@dfinity/utils";
import { TemplateResult } from "lit-html";
import type { UAParser } from "ua-parser-js";
import { tempKeyWarningBox } from "../manage/tempKeys";
import { setPinFlow } from "../pin/setPin";
import { precomputeFirst, promptCaptcha } from "./captcha";
import { displayUserNumberWarmup } from "./finish";
import { savePasskeyOrPin } from "./passkey";
Expand All @@ -33,9 +44,9 @@ export const registerFlow = async ({
identityRegistrationStart,
checkCaptcha,
identityRegistrationFinish,
storePinIdentity: _storePinIdentity,
storePinIdentity,
registrationAllowed,
pinAllowed: _pinAllowed,
pinAllowed,
uaParser,
}: {
identityRegistrationStart: () => Promise<
Expand Down Expand Up @@ -87,6 +98,7 @@ export const registerFlow = async ({
| RateLimitExceeded
| "canceled"
> => {
console.log("in da registerFlow");
sea-snake marked this conversation as resolved.
Show resolved Hide resolved
if (!registrationAllowed) {
const result = await registerDisabled();
result satisfies { tag: "canceled" };
Expand All @@ -98,24 +110,82 @@ export const registerFlow = async ({
const flowStart = precomputeFirst(() => identityRegistrationStart());

const displayUserNumber = displayUserNumberWarmup();
const identity = await savePasskeyOrPin();
if (identity === undefined) {
// TODO: Return something meaningful if getting the identity fails
const savePasskeyResult = await savePasskeyOrPin({
pinAllowed: await pinAllowed(),
});
if (savePasskeyResult === "canceled") {
return "canceled";
}
const alias = await inferPasskeyAlias({
authenticatorType: identity.getAuthenticatorAttachment(),
userAgent: navigator.userAgent,
uaParser,
});
const result_ = await (async () => {
if (savePasskeyResult === "pin") {
const pinResult = await setPinFlow();
if (pinResult.tag === "canceled") {
return "canceled";
}

const authnMethodData = passkeyAuthnMethodData({
alias,
pubKey: identity.getPublicKey().toDer(),
credentialId: identity.rawId,
authenticatorAttachment: identity.getAuthenticatorAttachment(),
});
const authnMethod = "passkey" as const;
pinResult.tag satisfies "ok";

// XXX: this withLoader could be replaced with one that indicates what's happening (like the
// "Hang tight, ..." spinner)
const { identity, pinIdentityMaterial } = await withLoader(() =>
constructPinIdentity(pinResult)
);
const alias = await inferPinAlias({
userAgent: navigator.userAgent,
uaParser,
});
return {
identity,
authnMethodData: pinAuthnMethodData({
alias,
pubKey: identity.getPublicKey().toDer(),
}),
finalizeIdentity: (userNumber: bigint) =>
storePinIdentity({ userNumber, pinIdentityMaterial }),
finishSlot: tempKeyWarningBox({ i18n: new I18n() }),
authnMethod: "pin" as const,
};
} else {
const identity = savePasskeyResult;
// TODO: Return something meaningful if getting the passkey identity fails
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only new line that was not just reverted. Because the view can return undefined.

This is in the current code as well. The way this error was handled before was to never resolve the promise and show a toast error.

if (identity === undefined) {
return "canceled";
}
const alias = await inferPasskeyAlias({
authenticatorType: identity.getAuthenticatorAttachment(),
userAgent: navigator.userAgent,
uaParser,
});
return {
identity,
authnMethodData: passkeyAuthnMethodData({
alias,
pubKey: identity.getPublicKey().toDer(),
credentialId: identity.rawId,
authenticatorAttachment: identity.getAuthenticatorAttachment(),
}),
authnMethod: "passkey" as const,
};
}
})();

if (result_ === "canceled") {
return "canceled";
}

const {
identity,
authnMethodData,
finalizeIdentity,
finishSlot,
authnMethod,
}: {
identity: SignIdentity;
authnMethodData: AuthnMethodData;
finalizeIdentity?: (userNumber: bigint) => Promise<void>;
finishSlot?: TemplateResult;
authnMethod: "pin" | "passkey";
} = result_;

const startResult = await flowStart();
if (startResult.kind !== "registrationFlowStepSuccess") {
Expand Down Expand Up @@ -150,6 +220,7 @@ export const registerFlow = async ({
result.kind satisfies "loginSuccess";

const userNumber = result.userNumber;
await finalizeIdentity?.(userNumber);
// We don't want to nudge the user with the recovery phrase warning page
// right after they've created their anchor.
result.connection.updateIdentityMetadata({
Expand All @@ -162,6 +233,7 @@ export const registerFlow = async ({
);
await displayUserNumber({
userNumber,
marketingIntroSlot: finishSlot,
});
return { ...result, authnMethod };
};
Expand All @@ -180,6 +252,10 @@ export const getRegisterFlowOpts = async ({
const tempIdentity = await ECDSAKeyIdentity.generate({
extractable: false,
});
const params = new URLSearchParams(window.location.search);
// Only allow PIN if query param is set and the request allows it
const allowPinRegistration =
params.get(ENABLE_PIN_QUERY_PARAM_KEY) !== null && allowPinAuthentication;
return {
/** Check that the current origin is not the explicit canister id or a raw url.
* Explanation why we need to do this:
Expand All @@ -192,7 +268,7 @@ export const getRegisterFlowOpts = async ({
pinAllowed: () =>
// If pin auth is disallowed by the authenticating dapp then abort, otherwise check
// if pin auth is allowed for the user agent
allowPinAuthentication
allowPinRegistration
? pinRegisterAllowed({ userAgent: navigator.userAgent, uaParser })
: Promise.resolve(false),
identityRegistrationStart: async () =>
Expand Down
29 changes: 25 additions & 4 deletions src/frontend/src/flows/register/passkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,32 @@ const savePasskeyTemplate = ({
export const savePasskeyPage = renderPage(savePasskeyTemplate);

// Prompt the user to create a WebAuthn identity or a PIN identity (if allowed)
export const savePasskeyOrPin = async (): Promise<
IIWebAuthnIdentity | undefined
> => {
export const savePasskeyOrPin = async ({
Copy link
Collaborator Author

@lmuntaner lmuntaner Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is reverted to the old logic if pinAllowed is true and skip to render the page if not enabled.

pinAllowed,
}: {
pinAllowed: boolean;
}): Promise<IIWebAuthnIdentity | "pin" | "canceled" | undefined> => {
if (pinAllowed) {
return new Promise((resolve) => {
return savePasskeyPage({
i18n: new I18n(),
cancel: () => resolve("canceled"),
scrollToTop: true,
constructPasskey: async () => {
try {
const identity = await withLoader(() => constructIdentity({}));
resolve(identity);
} catch (e) {
toast.error(errorMessage(e));
}
},
constructPin: pinAllowed ? () => resolve("pin") : undefined,
});
});
}
try {
return await withLoader(() => constructIdentity({}));
const identity = await withLoader(() => constructIdentity({}));
return identity;
} catch (e) {
toast.error(errorMessage(e));
return undefined;
Expand Down
Loading