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

Showcase registration flow #1836

Merged
merged 1 commit into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
61 changes: 14 additions & 47 deletions src/frontend/src/flows/register/captcha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,8 @@ import { Challenge } from "$generated/internet_identity_types";
import { mainWindow } from "$src/components/mainWindow";
import { DynamicKey, I18n } from "$src/i18n";
import { LoginFlowCanceled, cancel } from "$src/utils/flowResult";
import {
Connection,
IIWebAuthnIdentity,
RegisterResult,
} from "$src/utils/iiConnection";
import { mount, renderPage, withRef } from "$src/utils/lit-html";
import { Chan } from "$src/utils/utils";
import { ECDSAKeyIdentity } from "@dfinity/identity";
import { TemplateResult, html } from "lit-html";
import { asyncReplace } from "lit-html/directives/async-replace.js";
import { Ref, createRef, ref } from "lit-html/directives/ref.js";
Expand Down Expand Up @@ -262,62 +256,35 @@ export function promptCaptchaPage<T>(
)(props, container);
}

export const promptCaptcha = ({
connection,
identity,
alias,
challenge,
export const promptCaptcha = <T>({
createChallenge,
register,
}: {
connection: Connection;
identity: IIWebAuthnIdentity;
alias: string;
challenge?: Promise<Challenge>;
}): Promise<RegisterResult | LoginFlowCanceled> => {
createChallenge: () => Promise<Challenge>;
register: (cr: {
chars: string;
challenge: Challenge;
}) => Promise<T | typeof badChallenge>;
}): Promise<T | LoginFlowCanceled> => {
return new Promise((resolve) => {
const i18n = new I18n();
promptCaptchaPage({
verifyChallengeChars: register,
requestChallenge: () => createChallenge(),
cancel: () => resolve(cancel),
focus: true,
verifyChallengeChars: async ({ chars, challenge }) => {
const tempIdentity = await ECDSAKeyIdentity.generate({
extractable: false,
});
const result = await connection.register({
identity,
tempIdentity,
alias,
challengeResult: {
key: challenge.challenge_key,
chars,
},
});

switch (result.kind) {
case "badChallenge":
return badChallenge;
default:
return result;
}
},

requestChallenge: precomputedFirst(
// For the first call, use a pre-generated challenge
// if available.
challenge ?? connection.createChallenge(),
() => connection.createChallenge()
),

onContinue: resolve,
i18n,
scrollToTop: true,
focus: true,
});
});
};

// Returns a function that returns `first` on the first call,
// and values returned by `f()` from the second call on.
function precomputedFirst<T>(first: T, f: () => T): () => T {
export function precomputeFirst<T>(f: () => T): () => T {
let firstTime = true;
const first: T = f();

return () => {
if (firstTime) {
Expand Down
134 changes: 99 additions & 35 deletions src/frontend/src/flows/register/index.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,124 @@
import { Challenge } from "$generated/internet_identity_types";
import {
LoginFlowCanceled,
LoginFlowResult,
apiResultToLoginFlowResult,
cancel,
LoginFlowResult,
} from "$src/utils/flowResult";
import { Connection, IIWebAuthnIdentity } from "$src/utils/iiConnection";
import {
AuthenticatedConnection,
Connection,
IIWebAuthnIdentity,
RegisterResult,
} from "$src/utils/iiConnection";
import { setAnchorUsed } from "$src/utils/userNumber";
import { unknownToString } from "$src/utils/utils";
import { ECDSAKeyIdentity } from "@dfinity/identity";
import { nonNullish } from "@dfinity/utils";
import type { UAParser } from "ua-parser-js";
import { promptCaptcha } from "./captcha";
import { badChallenge, precomputeFirst, promptCaptcha } from "./captcha";
import { displayUserNumberWarmup } from "./finish";
import { savePasskey } from "./passkey";

/** Registration (anchor creation) flow for new users */
export const registerFlow = async <T>({
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

most of this is was just extracted from register

createChallenge: createChallenge_,
register,
}: {
createChallenge: () => Promise<Challenge>;
register: (opts: {
alias: string;
identity: IIWebAuthnIdentity;
challengeResult: { chars: string; challenge: Challenge };
}) => Promise<RegisterResult<T>>;
}): Promise<RegisterResult<T> | LoginFlowCanceled> => {
// Kick-off fetching "ua-parser-js";
const uaParser = loadUAParser();

// Kick-off the challenge request early, so that we might already
// have a captcha to show once we get to the CAPTCHA screen
const createChallenge = precomputeFirst(() => createChallenge_());

const identity = await savePasskey();
if (identity === "canceled") {
return cancel;
}

const alias = await inferAlias({
authenticatorType: identity.getAuthenticatorAttachment(),
userAgent: navigator.userAgent,
uaParser,
});

const displayUserNumber = displayUserNumberWarmup();

const result = await promptCaptcha({
createChallenge,
register: async ({ chars, challenge }) => {
const result = await register({
identity,
alias,
challengeResult: { chars, challenge },
});

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

return result;
},
});

if ("tag" in result) {
result.tag satisfies "canceled";
return result;
}

if (result.kind === "loginSuccess") {
const userNumber = result.userNumber;
setAnchorUsed(userNumber);
await displayUserNumber({ userNumber });
}
return result;
};

/** Concrete implementation of the registration flow */
export const register = async ({
connection,
}: {
connection: Connection;
}): Promise<LoginFlowResult> => {
try {
// Kick-off fetching "ua-parser-js";
const uaParser = loadUAParser();

// Kick-off the challenge request early, so that we might already
// have a captcha to show once we get to the CAPTCHA screen
const preloadedChallenge = connection.createChallenge();
const identity = await savePasskey();
if (identity === "canceled") {
return cancel;
}

const alias = await inferAlias({
authenticatorType: identity.getAuthenticatorAttachment(),
userAgent: navigator.userAgent,
uaParser,
const result = await registerFlow<AuthenticatedConnection>({
createChallenge: () => connection.createChallenge(),
register: async ({
identity,
alias,
challengeResult: {
chars,
challenge: { challenge_key: key },
},
}) => {
const tempIdentity = await ECDSAKeyIdentity.generate({
extractable: false,
});
const result = await connection.register({
identity,
tempIdentity,
alias,
challengeResult: { chars, key },
});

return result;
},
});

const displayUserNumber = displayUserNumberWarmup();
const captchaResult = await promptCaptcha({
connection,
challenge: preloadedChallenge,
identity,
alias,
});

if ("tag" in captchaResult) {
return captchaResult;
} else {
const result = apiResultToLoginFlowResult(captchaResult);
if (result.tag === "ok") {
const { userNumber } = result;
setAnchorUsed(userNumber);
await displayUserNumber({ userNumber });
}
if ("tag" in result) {
result satisfies { tag: "canceled" };
return result;
}

return apiResultToLoginFlowResult(result);
} catch (e) {
return {
tag: "err",
Expand Down
8 changes: 4 additions & 4 deletions src/frontend/src/utils/iiConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,17 @@ export type LoginResult =
| NoSeedPhrase
| SeedPhraseFail
| CancelOrTimeout;
export type RegisterResult =
| LoginSuccess
export type RegisterResult<T = AuthenticatedConnection> =
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

these types are a mess but I don't have time to clean this up right now

| LoginSuccess<T>
| AuthFail
| ApiError
| RegisterNoSpace
| BadChallenge
| CancelOrTimeout;

type LoginSuccess = {
type LoginSuccess<T = AuthenticatedConnection> = {
kind: "loginSuccess";
connection: AuthenticatedConnection;
connection: T;
userNumber: bigint;
};

Expand Down
52 changes: 52 additions & 0 deletions src/showcase/src/flows.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { toast } from "$src/components/toast";
import { registerFlow } from "$src/flows/register";
import { badChallenge, promptCaptchaPage } from "$src/flows/register/captcha";
import { TemplateResult, html, render } from "lit-html";
import { dummyChallenge, i18n } from "./showcase";
Expand Down Expand Up @@ -25,6 +27,40 @@ export const iiFlows: Record<string, () => void> = {
focus: false,
});
},

register: async () => {
const result = await registerFlow<null>({
createChallenge: async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
return dummyChallenge;
},
register: async ({ challengeResult: { chars } }) => {
await new Promise((resolve) => setTimeout(resolve, 2000));

if (chars !== "8wJ6Q") {
return { kind: "badChallenge" };
}
return {
kind: "loginSuccess",
userNumber: BigInt(12356),
connection: null,
};
},
});

toast.success(html`
Identity successfully created!<br />
<p class="l-stack">
<strong class="t-strong">${prettyResult(result)}</strong>
</p>
<button
class="l-stack c-button c-button--secondary"
@click=${() => window.location.reload()}
>
reload
</button>
`);
},
};

const pageContent: TemplateResult = html`
Expand All @@ -49,3 +85,19 @@ const pageContent: TemplateResult = html`
</div>
</div>
`;

const prettyResult = (obj: unknown) => {
if (typeof obj === "string") {
return obj;
}

if (typeof obj === "object" && obj !== null) {
return html`<ul>
${Object.entries(obj).map(
([k, v]) => html`<li><strong class="t-strong">${k}: ${v}</strong></li>`
)}
</ul>`;
}

return JSON.stringify(obj);
};