2FA choice place 1

This commit is contained in:
Manav Rathi
2024-11-28 11:14:12 +05:30
parent d2761b6be9
commit ee34ed5e1f
4 changed files with 131 additions and 3 deletions

View File

@@ -0,0 +1,60 @@
import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton";
import type { ModalVisibilityProps } from "@/base/components/utils/modal";
import { Dialog, DialogContent, DialogTitle, Stack } from "@mui/material";
import { t } from "i18next";
import React from "react";
export type SecondFactorType = "totp" | "passkey";
type SecondFactorChoiceProps = ModalVisibilityProps & {
/**
* Callback invoked with the selected choice.
*
* The dialog will automatically be closed before this callback is invoked.
*/
didSelect: (factor: SecondFactorType) => void;
};
/**
* A {@link Dialog} that allow the user to choose which second factor they'd
* like to verify during login.
*/
export const SecondFactorChoice: React.FC<SecondFactorChoiceProps> = ({
open,
onClose,
didSelect,
}) => (
<Dialog
open={open}
onClose={(_, reason) => {
if (reason != "backdropClick") onClose();
}}
fullWidth
PaperProps={{ sx: { maxWidth: "360px", padding: "12px" } }}
>
<DialogTitle>{t("two_factor")}</DialogTitle>
<DialogContent>
<Stack sx={{ gap: "12px" }}>
<FocusVisibleButton
color="accent"
onClick={() => {
onClose();
didSelect("totp");
}}
>
{t("totp_login")}
</FocusVisibleButton>
<FocusVisibleButton
color="accent"
onClick={() => {
onClose();
didSelect("passkey");
}}
>
{t("passkey_login")}
</FocusVisibleButton>
</Stack>
</DialogContent>
</Dialog>
);

View File

@@ -1,6 +1,7 @@
import { sessionExpiredDialogAttributes } from "@/accounts/components/utils/dialog";
import { FormPaper } from "@/base/components/FormPaper";
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
import { useModalVisibility } from "@/base/components/utils/modal";
import { sharedCryptoWorker } from "@/base/crypto";
import type { B64EncryptionResult } from "@/base/crypto/libsodium";
import { clearLocalStorage } from "@/base/local-storage";
@@ -38,12 +39,16 @@ import type { KeyAttributes, User } from "@ente/shared/user/types";
import { Stack } from "@mui/material";
import { t } from "i18next";
import { useRouter } from "next/router";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import {
LoginFlowFormFooter,
PasswordHeader,
VerifyingPasskey,
} from "../components/LoginComponents";
import {
SecondFactorChoice,
type SecondFactorType,
} from "../components/SecondFactorChoice";
import { PAGES } from "../constants/pages";
import {
openPasskeyVerificationURL,
@@ -76,6 +81,14 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
const [sessionValidityCheck, setSessionValidityCheck] = useState<
Promise<void> | undefined
>();
const resolveSecondFactorChoice = useRef<
| ((value: SecondFactorType | PromiseLike<SecondFactorType>) => void)
| undefined
>();
const {
show: showSecondFactorChoice,
props: secondFactorChoiceVisibilityProps,
} = useModalVisibility();
const router = useRouter();
@@ -216,10 +229,47 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
encryptedToken,
token,
id,
twoFactorSessionID,
passkeySessionID,
twoFactorSessionID: _twoFactorSessionIDV1,
twoFactorSessionIDV2: _twoFactorSessionIDV2,
passkeySessionID: _passkeySessionID,
} = await loginViaSRP(srpAttributes!, kek);
setIsFirstLogin(true);
// When the user has both TOTP and pk set as the second factor,
// we'll get two session IDs. For backward compat, the TOTP
// session ID will be in a V2 attribute during a transient
// migration period.
//
// Note the use of || instead of ?? since _twoFactorSessionIDV1
// will be an empty string, not undefined, if it is unset. We
// might need to add a `xxx-eslint-disable
// @typescript-eslint/prefer-nullish-coalescing` here too later.
const _twoFactorSessionID =
_twoFactorSessionIDV1 || _twoFactorSessionIDV2;
let passkeySessionID: string | undefined;
let twoFactorSessionID: string | undefined;
if (_twoFactorSessionID && _passkeySessionID) {
// If both factors are set, ask the user which one they wish
// to use.
const choice = await new Promise<SecondFactorType>(
(resolve) => {
resolveSecondFactorChoice.current = resolve;
showSecondFactorChoice();
},
);
switch (choice) {
case "passkey":
passkeySessionID = _passkeySessionID;
break;
case "totp":
twoFactorSessionID = _twoFactorSessionID;
break;
}
} else {
passkeySessionID = _passkeySessionID;
twoFactorSessionID = _twoFactorSessionID;
}
if (passkeySessionID) {
const sessionKeyAttributes =
await cryptoWorker.generateKeyAndEncryptToB64(kek);
@@ -322,6 +372,12 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
}
};
const handleSecondFactorChoice = (factor: SecondFactorType) => {
const resolve = resolveSecondFactorChoice.current!;
resolveSecondFactorChoice.current = undefined;
resolve(factor);
};
if (!keyAttributes && !srpAttributes) {
return (
<VerticallyCentered>
@@ -387,6 +443,11 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
</Stack>
</LoginFlowFormFooter>
</FormPaper>
<SecondFactorChoice
{...secondFactorChoiceVisibilityProps}
didSelect={handleSecondFactorChoice}
/>
</VerticallyCentered>
);
};

View File

@@ -13,6 +13,12 @@ export interface UserVerificationResponse {
token?: string;
twoFactorSessionID: string;
passkeySessionID: string;
/**
* If both passkeys and TOTP based two factors are enabled, then {@link
* twoFactorSessionIDV2} will be set to the TOTP session ID instead of
* {@link twoFactorSessionID}.
*/
twoFactorSessionIDV2?: string | undefined;
srpM2?: string;
}

View File

@@ -637,6 +637,7 @@
"check_status": "Check status",
"passkey_login_instructions": "Follow the steps from your browser to continue logging in.",
"passkey_login": "Login with passkey",
"totp_login": "Login with TOTP",
"passkey": "Passkey",
"passkey_verify_description": "Verify your passkey to login into your account.",
"waiting_for_verification": "Waiting for verification...",