From 0c5065e35a85a674ab085f910adf586362c15100 Mon Sep 17 00:00:00 2001 From: Frederik Rothenberger Date: Wed, 2 Oct 2024 17:19:37 +0200 Subject: [PATCH] Wip --- .../components/authenticateBox/errorToast.ts | 28 +- .../src/components/authenticateBox/index.ts | 21 +- src/frontend/src/flows/register/captcha.ts | 127 +++++---- src/frontend/src/flows/register/index.ts | 188 +++++++------ src/frontend/src/utils/authnMethodData.ts | 83 ++++++ src/frontend/src/utils/iiConnection.ts | 246 +++++++++++++++++- src/showcase/src/flows.ts | 28 +- 7 files changed, 554 insertions(+), 167 deletions(-) create mode 100644 src/frontend/src/utils/authnMethodData.ts diff --git a/src/frontend/src/components/authenticateBox/errorToast.ts b/src/frontend/src/components/authenticateBox/errorToast.ts index c2ae5ba639..a7eca778e6 100644 --- a/src/frontend/src/components/authenticateBox/errorToast.ts +++ b/src/frontend/src/components/authenticateBox/errorToast.ts @@ -11,7 +11,7 @@ type KindToError = Omit< "kind" >; -// Makes the error human readable +// Makes the error human-readable const clarifyError: { [K in FlowError["kind"]]: (err: KindToError) => { title: string; @@ -56,6 +56,32 @@ const clarifyError: { message: "The Dapp you are authenticating to does not allow PIN identities and you only have a PIN identity. Please retry using a Passkey: open a new Internet Identity page, add a passkey and retry.", }), + alreadyInProgress: () => ({ + title: "Registration is already in progress", + message: "Registration has already been started on this session.", + }), + rateLimitExceeded: () => ({ + title: "Registration rate limit exceeded", + message: + "Internet Identity is under heavy load. Too many registrations. Please try again later.", + }), + invalidCaller: () => ({ + title: "Registration is not allowed using the anonymous identity", + message: + "Registration was attempted using the anonymous identity which is not allowed.", + }), + invalidAuthnMethod: (err) => ({ + title: "Invalid authentication method", + message: `Invalid authentication method: ${err.message}.`, + }), + noRegistrationFlow: () => ({ + title: "Registration flow timed out", + message: "Registration flow timed out. Please restart.", + }), + unexpectedCall: (err) => ({ + title: "Unexpected call", + message: `Unexpected call: expected next step "${err.nextStep.step}"`, + }), }; export const flowErrorToastTemplate = ( diff --git a/src/frontend/src/components/authenticateBox/index.ts b/src/frontend/src/components/authenticateBox/index.ts index d6641451c3..998549563f 100644 --- a/src/frontend/src/components/authenticateBox/index.ts +++ b/src/frontend/src/components/authenticateBox/index.ts @@ -23,14 +23,20 @@ import { import { I18n } from "$src/i18n"; import { getAnchors, setAnchorUsed } from "$src/storage"; import { + AlreadyInProgress, ApiError, AuthFail, AuthenticatedConnection, BadChallenge, BadPin, Connection, + InvalidAuthnMethod, + InvalidCaller, LoginSuccess, + NoRegistrationFlow, + RateLimitExceeded, RegisterNoSpace, + UnexpectedCall, UnknownUser, WebAuthnFailed, bufferEqual, @@ -80,7 +86,7 @@ export const authenticateBox = async ({ newAnchor: boolean; authnMethod: "pin" | "passkey" | "recovery"; }> => { - const promptAuth = (autoSelectIdentity?: bigint) => + const promptAuth = async (autoSelectIdentity?: bigint) => authenticateBoxFlow({ i18n, templates, @@ -89,7 +95,7 @@ export const authenticateBox = async ({ loginPinIdentityMaterial: (opts) => loginPinIdentityMaterial({ ...opts, connection }), recover: () => useRecovery(connection), - registerFlowOpts: getRegisterFlowOpts({ + registerFlowOpts: await getRegisterFlowOpts({ connection, allowPinAuthentication, }), @@ -223,10 +229,7 @@ export const authenticateBoxFlow = async ({ newAnchor: true; authnMethod: "pin" | "passkey" | "recovery"; }) - | BadChallenge - | ApiError - | AuthFail - | RegisterNoSpace + | FlowError | { tag: "canceled" } > => { const result2 = await registerFlow(registerFlowOpts); @@ -333,6 +336,12 @@ export type FlowError = | WebAuthnFailed | UnknownUser | ApiError + | InvalidCaller + | AlreadyInProgress + | RateLimitExceeded + | NoRegistrationFlow + | UnexpectedCall + | InvalidAuthnMethod | RegisterNoSpace; export const handleLoginFlowResult = async ( diff --git a/src/frontend/src/flows/register/captcha.ts b/src/frontend/src/flows/register/captcha.ts index de85eb2ca1..370b2b81d5 100644 --- a/src/frontend/src/flows/register/captcha.ts +++ b/src/frontend/src/flows/register/captcha.ts @@ -1,23 +1,18 @@ -import { Challenge } from "$generated/internet_identity_types"; import { mainWindow } from "$src/components/mainWindow"; import { DynamicKey, I18n } from "$src/i18n"; +import { WrongCaptchaSolution } from "$src/utils/iiConnection"; import { mount, renderPage, withRef } from "$src/utils/lit-html"; import { Chan } from "$src/utils/utils"; +import { isNullish, nonNullish } from "@dfinity/utils"; 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"; - -import { isNullish, nonNullish } from "@dfinity/utils"; import copyJson from "./captcha.json"; -// A symbol that we can differentiate from generic `T` types -// when verifying the challenge -export const badChallenge: unique symbol = Symbol("ii.bad_challenge"); - export const promptCaptchaTemplate = ({ cancel, - requestChallenge, - verifyChallengeChars, + captcha_png_base64, + checkCaptcha, onContinue, i18n, stepper, @@ -25,12 +20,9 @@ export const promptCaptchaTemplate = ({ scrollToTop = false, }: { cancel: () => void; - requestChallenge: () => Promise; - verifyChallengeChars: (cr: { - chars: string; - challenge: Challenge; - }) => Promise; - onContinue: (result: T) => void; + captcha_png_base64: string; + checkCaptcha: (solution: string) => Promise | WrongCaptchaSolution>; + onContinue: (result: Exclude) => void; i18n: I18n; stepper: TemplateResult; focus?: boolean; @@ -62,7 +54,7 @@ export const promptCaptchaTemplate = ({ // The various states the component can inhabit type State = | { status: "requesting" } - | { status: "prompting"; challenge: Challenge } + | { status: "prompting"; captcha_png_base64: string } | { status: "verifying" } | { status: "bad" }; @@ -76,7 +68,7 @@ export const promptCaptchaTemplate = ({ state.status === "requesting" ? spinnerImg : state.status === "prompting" - ? captchaImg(state.challenge.png_base64) + ? captchaImg(state.captcha_png_base64) : Chan.unchanged, def: spinnerImg, }); @@ -110,57 +102,55 @@ export const promptCaptchaTemplate = ({ ? (e) => { e.preventDefault(); e.stopPropagation(); - doVerify(state.challenge); + doVerify(); } : undefined ); const nextDisabled: Chan = next.map(isNullish); - const nextCaption: Chan = state.map(({ status }) => - status === "requesting" - ? copy.generating - : status === "verifying" - ? copy.verifying - : copy.next - ); - - // The "retry" button behavior - const retry: Chan<(() => Promise) | undefined> = state.map((state) => - state.status === "prompting" || state.status === "bad" ? doRetry : undefined - ); - const retryDisabled: Chan = retry.map(isNullish); - - // On retry, request a new challenge - const doRetry = async () => { - state.send({ status: "requesting" }); - const challenge = await requestChallenge(); - state.send({ status: "prompting", challenge }); - }; + const nextCaption: Chan = state.map(({ status }) => { + console.log("next caption", status); + if (status === "requesting") { + return copy.generating; + } + if (status === "verifying") { + return copy.verifying; + } + console.log("copy next", copy.next); + return copy.next; + }); + // On retry, prompt with a new challenge // On verification, check the chars and either continue (on good challenge) // or go to "bad" state - const doVerify = (challenge: Challenge) => { + const doVerify = () => { state.send({ status: "verifying" }); void withRef(input, async (input) => { - const res = await verifyChallengeChars({ - chars: input.value, - challenge, - }); - if (res === badChallenge) { + const res = await checkCaptcha(input.value); + if (isBadCaptchaResult(res)) { + console.log("bad captcha"); // on a bad challenge, show some error, clear the input & focus // and retry state.send({ status: "bad" }); input.value = ""; input.focus(); - void doRetry(); - } else { - onContinue(res); + console.log("sending prompting") + state.send({ status: "requesting", }); + state.send({ + status: "prompting", + captcha_png_base64: res.new_captcha_png_base64, + }); + return; } + onContinue(res); }); }; // Kickstart everything - void doRetry(); + void state.send({ + status: "prompting", + captcha_png_base64: captcha_png_base64, + }); // A "resize" handler than ensures that the captcha is centered when after // the page is resized. This is particularly useful on mobile devices, where @@ -192,15 +182,6 @@ export const promptCaptchaTemplate = ({ class="c-input c-input--icon" > ${asyncReplace(img)} - - ${copy.retry} -