From 417326d55c9df47416096f742c3dbaad035de359 Mon Sep 17 00:00:00 2001 From: Nicolas Mattia Date: Fri, 1 Sep 2023 16:01:23 +0200 Subject: [PATCH] Showcase login flow This extracts the flow essence of the authenticateBox so that the flow can be showcased on `/flows/loginManage`. In practice, most canister calls were abstracted away so that the flow can be used without an actual canister. Similar to the registration flow showcased in https://github.com/dfinity/internet-identity/pull/1836. --- .../src/components/authenticateBox.ts | 73 ++++++++++++++----- src/frontend/src/flows/authorize/index.ts | 8 +- src/frontend/src/flows/manage/index.ts | 6 +- src/frontend/src/index.ts | 6 +- src/frontend/src/utils/flowResult.ts | 18 ++--- src/frontend/src/utils/iiConnection.ts | 8 +- src/showcase/src/flows.ts | 49 ++++++++++++- src/showcase/src/showcase.ts | 2 +- 8 files changed, 129 insertions(+), 41 deletions(-) diff --git a/src/frontend/src/components/authenticateBox.ts b/src/frontend/src/components/authenticateBox.ts index 6f6862bb92..3501228cad 100644 --- a/src/frontend/src/components/authenticateBox.ts +++ b/src/frontend/src/components/authenticateBox.ts @@ -8,7 +8,7 @@ import { LoginFlowResult, LoginFlowSuccess, } from "$src/utils/flowResult"; -import { Connection } from "$src/utils/iiConnection"; +import { Connection, LoginResult } from "$src/utils/iiConnection"; import { TemplateElement, withRef } from "$src/utils/lit-html"; import { registerIfAllowed } from "$src/utils/registerAllowedCheck"; import { @@ -52,34 +52,66 @@ export type AuthnTemplates = { }; }; +export const authenticateBox = ({ + connection, + i18n, + templates, +}: { + connection: Connection; + i18n: I18n; + templates: AuthnTemplates; +}): Promise => { + return authenticateBoxFlow({ + i18n, + templates, + addDevice: (userNumber) => asNewDevice(connection, userNumber), + login: (userNumber) => + handleLogin({ + login: () => connection.login(userNumber), + }), + register: () => registerIfAllowed(connection), + recover: () => useRecovery(connection), + }); +}; + /** Authentication box component which authenticates a user * to II or to another dapp */ -export const authenticateBox = async ( - connection: Connection, - i18n: I18n, - templates: AuthnTemplates -): Promise => { +export const authenticateBoxFlow = async ({ + i18n, + templates, + addDevice, + login, + register, + recover, +}: { + i18n: I18n; + templates: AuthnTemplates; + addDevice: (userNumber?: bigint) => Promise<{ alias: string }>; + login: (userNumber: bigint) => Promise | LoginFlowError>; + register: () => Promise>; + recover: () => Promise>; +}): Promise & { newAnchor: boolean }> => { const promptAuth = () => - new Promise((resolve) => { + new Promise & { newAnchor: boolean }>((resolve) => { const pages = authnPages(i18n, { ...templates, - addDevice: (userNumber) => asNewDevice(connection, userNumber), + addDevice: (userNumber) => addDevice(userNumber), onSubmit: async (userNumber) => { resolve({ newAnchor: false, - ...(await authenticate(connection, userNumber)), + ...(await login(userNumber)), }); }, register: async () => { resolve({ - ...(await registerIfAllowed(connection)), newAnchor: true, + ...(await register()), }); }, recover: async (_userNumber) => { resolve({ - ...(await useRecovery(connection)), newAnchor: false, + ...(await recover()), }); }, }); @@ -108,9 +140,9 @@ export const authenticateBox = async ( } }; -export const handleLoginFlowResult = async ( - result: LoginFlowResult -): Promise => { +export const handleLoginFlowResult = async ( + result: LoginFlowResult +): Promise | undefined> => { switch (result.tag) { case "ok": setAnchorUsed(result.userNumber); @@ -300,13 +332,14 @@ export const authnPages = ( }; }; -export const authenticate = async ( - connection: Connection, - userNumber: bigint -): Promise => { +export const handleLogin = async ({ + login, +}: { + login: () => Promise>; +}): Promise | LoginFlowError> => { try { - const result = await withLoader(() => connection.login(userNumber)); - return apiResultToLoginFlowResult(result); + const result = await withLoader(() => login()); + return apiResultToLoginFlowResult(result); } catch (error) { return { tag: "err", diff --git a/src/frontend/src/flows/authorize/index.ts b/src/frontend/src/flows/authorize/index.ts index 2af2d7778c..5b40b1bca3 100644 --- a/src/frontend/src/flows/authorize/index.ts +++ b/src/frontend/src/flows/authorize/index.ts @@ -159,18 +159,18 @@ export const authFlowAuthorize = async ( showSpinner({ message: copy.starting_authentication }); const result = await authenticationProtocol({ authenticate: async (authContext) => { - const authSuccess = await authenticateBox( + const authSuccess = await authenticateBox({ connection, i18n, - authnTemplateAuthorize({ + templates: authnTemplateAuthorize({ origin: authContext.requestOrigin, derivationOrigin: authContext.authRequest.derivationOrigin, i18n, knownDapp: getDapps().find((dapp) => dapp.hasOrigin(authContext.requestOrigin) ), - }) - ); + }), + }); // Here, if the user is returning & doesn't have any recovery device, we prompt them to add // one. The exact flow depends on the device they use. diff --git a/src/frontend/src/flows/manage/index.ts b/src/frontend/src/flows/manage/index.ts index 092c107519..a2f913b6cc 100644 --- a/src/frontend/src/flows/manage/index.ts +++ b/src/frontend/src/flows/manage/index.ts @@ -106,7 +106,11 @@ export const authFlowManage = async (connection: Connection) => { userNumber, connection: authenticatedConnection, newAnchor, - } = await authenticateBox(connection, i18n, authnTemplateManage({ dapps })); + } = await authenticateBox({ + connection, + i18n, + templates: authnTemplateManage({ dapps }), + }); // Here, if the user is returning & doesn't have any recovery device, we prompt them to add // one. The exact flow depends on the device they use. diff --git a/src/frontend/src/index.ts b/src/frontend/src/index.ts index 50022f5010..0e4b1f0592 100644 --- a/src/frontend/src/index.ts +++ b/src/frontend/src/index.ts @@ -16,7 +16,7 @@ import { isNullish, nonNullish } from "@dfinity/utils"; // Polyfill Buffer globally for the browser import { - authenticate, + handleLogin, handleLoginFlowResult, } from "$src/components/authenticateBox"; import { Buffer } from "buffer"; @@ -117,7 +117,9 @@ const init = async () => { const renderManage = renderManageWarmup(); // If user "Click" continue in success page, proceed with authentication - const result = await authenticate(connection, userNumber); + const result = await handleLogin({ + login: () => connection.login(userNumber), + }); const loginData = await handleLoginFlowResult(result); // User have successfully signed-in we can jump to manage page diff --git a/src/frontend/src/utils/flowResult.ts b/src/frontend/src/utils/flowResult.ts index 5cf6d4213e..5a7b25699e 100644 --- a/src/frontend/src/utils/flowResult.ts +++ b/src/frontend/src/utils/flowResult.ts @@ -2,18 +2,18 @@ import { DynamicKey } from "$src/utils/i18n"; import { ApiResult, AuthenticatedConnection } from "./iiConnection"; import { webAuthnErrorCopy } from "./webAuthnErrorUtils"; -export type LoginFlowResult = - | LoginFlowSuccess +export type LoginFlowResult = + | LoginFlowSuccess | LoginFlowError | LoginFlowCanceled; -export type LoginFlowSuccess = { +export type LoginFlowSuccess = { tag: "ok"; -} & LoginData; +} & LoginData; -export type LoginData = { +export type LoginData = { userNumber: bigint; - connection: AuthenticatedConnection; + connection: T; }; export type LoginFlowError = { @@ -30,9 +30,9 @@ export type LoginError = { export type LoginFlowCanceled = { tag: "canceled" }; export const cancel: LoginFlowCanceled = { tag: "canceled" }; -export const apiResultToLoginFlowResult = ( - result: ApiResult -): LoginFlowSuccess | LoginFlowError => { +export const apiResultToLoginFlowResult = ( + result: ApiResult +): LoginFlowSuccess | LoginFlowError => { switch (result.kind) { case "loginSuccess": { return { diff --git a/src/frontend/src/utils/iiConnection.ts b/src/frontend/src/utils/iiConnection.ts index 61433abd7a..2ee65c3f72 100644 --- a/src/frontend/src/utils/iiConnection.ts +++ b/src/frontend/src/utils/iiConnection.ts @@ -74,9 +74,11 @@ export class DummyIdentity export const IC_DERIVATION_PATH = [44, 223, 0, 0, 0]; -export type ApiResult = LoginResult | RegisterResult; -export type LoginResult = - | LoginSuccess +export type ApiResult = + | LoginResult + | RegisterResult; +export type LoginResult = + | LoginSuccess | UnknownUser | AuthFail | ApiError diff --git a/src/showcase/src/flows.ts b/src/showcase/src/flows.ts index 186413abd3..4be72bfa23 100644 --- a/src/showcase/src/flows.ts +++ b/src/showcase/src/flows.ts @@ -1,8 +1,9 @@ +import { authenticateBoxFlow } from "$src/components/authenticateBox"; 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"; +import { dummyChallenge, i18n, manageTemplates } from "./showcase"; export const flowsPage = () => { document.title = "Flows"; @@ -11,6 +12,52 @@ export const flowsPage = () => { }; export const iiFlows: Record void> = { + loginManage: async () => { + const result = await authenticateBoxFlow({ + i18n, + templates: manageTemplates, + addDevice: () => { + toast.info(html`Added device`); + return Promise.resolve({ alias: "My Device" }); + }, + login: () => { + toast.info(html`Logged in`); + return Promise.resolve({ + tag: "ok", + userNumber: BigInt(1234), + connection: null, + }); + }, + register: () => { + toast.info(html`Registered`); + return Promise.resolve({ + tag: "ok", + userNumber: BigInt(1234), + connection: null, + }); + }, + recover: () => { + toast.info(html`Recovered`); + return Promise.resolve({ + tag: "ok", + userNumber: BigInt(1234), + connection: null, + }); + }, + }); + toast.success(html` + Authentication complete!
+

+ ${prettyResult(result)} +

+ + `); + }, captcha: () => { promptCaptchaPage({ cancel: () => console.log("canceled"), diff --git a/src/showcase/src/showcase.ts b/src/showcase/src/showcase.ts index 57d8740de5..6d415212b8 100644 --- a/src/showcase/src/showcase.ts +++ b/src/showcase/src/showcase.ts @@ -130,7 +130,7 @@ const authzKnownAlt = authnPages(i18n, { ...authzTemplatesKnownAlt, }); -const manageTemplates = authnTemplateManage({ dapps }); +export const manageTemplates = authnTemplateManage({ dapps }); const manage = authnPages(i18n, { ...authnCnfg, ...manageTemplates }); export const iiPages: Record void> = {