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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Showcase login flow #1841

Merged
merged 1 commit into from
Sep 1, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
73 changes: 53 additions & 20 deletions src/frontend/src/components/authenticateBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -52,34 +52,66 @@ export type AuthnTemplates = {
};
};

export const authenticateBox = ({
connection,
i18n,
templates,
}: {
connection: Connection;
i18n: I18n;
templates: AuthnTemplates;
}): Promise<LoginData & { newAnchor: boolean }> => {
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<LoginData & { newAnchor: boolean }> => {
export const authenticateBoxFlow = async <T>({
i18n,
templates,
addDevice,
login,
register,
recover,
}: {
i18n: I18n;
templates: AuthnTemplates;
addDevice: (userNumber?: bigint) => Promise<{ alias: string }>;
login: (userNumber: bigint) => Promise<LoginFlowSuccess<T> | LoginFlowError>;
register: () => Promise<LoginFlowResult<T>>;
recover: () => Promise<LoginFlowResult<T>>;
}): Promise<LoginData<T> & { newAnchor: boolean }> => {
const promptAuth = () =>
new Promise<LoginFlowResult & { newAnchor: boolean }>((resolve) => {
new Promise<LoginFlowResult<T> & { 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()),
});
},
});
Expand Down Expand Up @@ -108,9 +140,9 @@ export const authenticateBox = async (
}
};

export const handleLoginFlowResult = async (
result: LoginFlowResult
): Promise<LoginData | undefined> => {
export const handleLoginFlowResult = async <T>(
result: LoginFlowResult<T>
): Promise<LoginData<T> | undefined> => {
switch (result.tag) {
case "ok":
setAnchorUsed(result.userNumber);
Expand Down Expand Up @@ -300,13 +332,14 @@ export const authnPages = (
};
};

export const authenticate = async (
connection: Connection,
userNumber: bigint
): Promise<LoginFlowSuccess | LoginFlowError> => {
export const handleLogin = async <T>({
login,
}: {
login: () => Promise<LoginResult<T>>;
}): Promise<LoginFlowSuccess<T> | LoginFlowError> => {
try {
const result = await withLoader(() => connection.login(userNumber));
return apiResultToLoginFlowResult(result);
const result = await withLoader(() => login());
return apiResultToLoginFlowResult<T>(result);
} catch (error) {
return {
tag: "err",
Expand Down
8 changes: 4 additions & 4 deletions src/frontend/src/flows/authorize/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion src/frontend/src/flows/manage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions src/frontend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions src/frontend/src/utils/flowResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T = AuthenticatedConnection> =
| LoginFlowSuccess<T>
| LoginFlowError
| LoginFlowCanceled;

export type LoginFlowSuccess = {
export type LoginFlowSuccess<T = AuthenticatedConnection> = {
tag: "ok";
} & LoginData;
} & LoginData<T>;

export type LoginData = {
export type LoginData<T = AuthenticatedConnection> = {
userNumber: bigint;
connection: AuthenticatedConnection;
connection: T;
};

export type LoginFlowError = {
Expand All @@ -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 = <T>(
result: ApiResult<T>
): LoginFlowSuccess<T> | LoginFlowError => {
switch (result.kind) {
case "loginSuccess": {
return {
Expand Down
8 changes: 5 additions & 3 deletions src/frontend/src/utils/iiConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T = AuthenticatedConnection> =
| LoginResult<T>
| RegisterResult<T>;
export type LoginResult<T = AuthenticatedConnection> =
| LoginSuccess<T>
| UnknownUser
| AuthFail
| ApiError
Expand Down
49 changes: 48 additions & 1 deletion src/showcase/src/flows.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -11,6 +12,52 @@ export const flowsPage = () => {
};

export const iiFlows: Record<string, () => void> = {
loginManage: async () => {
const result = await authenticateBoxFlow<null>({
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!<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>
`);
},
captcha: () => {
promptCaptchaPage({
cancel: () => console.log("canceled"),
Expand Down
2 changes: 1 addition & 1 deletion src/showcase/src/showcase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, () => void> = {
Expand Down