diff --git a/web/src/App.tsx b/web/src/App.tsx index f0cabee9b..cdb3a3334 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -27,7 +27,6 @@ import { getResetPasswordCustomURL, getTheme, } from "@utils/Configuration"; -import RegisterOneTimePassword from "@views/DeviceRegistration/RegisterOneTimePassword"; import BaseLoadingPage from "@views/LoadingPage/BaseLoadingPage"; import ConsentView from "@views/LoginPortal/ConsentView/ConsentView"; import LoginPortal from "@views/LoginPortal/LoginPortal"; @@ -90,7 +89,6 @@ const App: React.FC = (props: Props) => { } /> } /> - } /> } /> } /> } /> diff --git a/web/src/services/Api.ts b/web/src/services/Api.ts index d0a618881..29b2af087 100644 --- a/web/src/services/Api.ts +++ b/web/src/services/Api.ts @@ -8,8 +8,6 @@ const basePath = getBasePath(); export const ConsentPath = basePath + "/api/oidc/consent"; export const FirstFactorPath = basePath + "/api/firstfactor"; -export const InitiateTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/start"; -export const CompleteTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/finish"; export const TOTPRegistrationOptionsPath = basePath + "/api/secondfactor/totp/register/options"; export const TOTPRegistrationPath = basePath + "/api/secondfactor/totp/register"; diff --git a/web/src/services/RegisterDevice.ts b/web/src/services/RegisterDevice.ts index 6631e8673..1db7844d4 100644 --- a/web/src/services/RegisterDevice.ts +++ b/web/src/services/RegisterDevice.ts @@ -1,19 +1,11 @@ -import { CompleteTOTPRegistrationPath, InitiateTOTPRegistrationPath, TOTPRegistrationPath } from "@services/Api"; -import { Post, PostWithOptionalResponse, Put } from "@services/Client"; - -export async function initiateTOTPRegistrationProcess() { - await PostWithOptionalResponse(InitiateTOTPRegistrationPath); -} +import { TOTPRegistrationPath } from "@services/Api"; +import { Put } from "@services/Client"; interface CompleteTOTPRegistrationResponse { base32_secret: string; otpauth_url: string; } -export async function completeTOTPRegistrationProcess(processToken: string) { - return Post(CompleteTOTPRegistrationPath, { token: processToken }); -} - export async function getTOTPSecret(algorithm: string, length: number, period: number) { return Put(TOTPRegistrationPath, { algorithm: algorithm, diff --git a/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx b/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx deleted file mode 100644 index 059553325..000000000 --- a/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; - -import { IconDefinition, faCopy, faKey, faTimesCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Box, Button, CircularProgress, IconButton, Link, TextField, Theme, Typography } from "@mui/material"; -import { red } from "@mui/material/colors"; -import makeStyles from "@mui/styles/makeStyles"; -import classnames from "classnames"; -import { QRCodeSVG } from "qrcode.react"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; - -import AppStoreBadges from "@components/AppStoreBadges"; -import { GoogleAuthenticator } from "@constants/constants"; -import { IndexRoute } from "@constants/Routes"; -import { IdentityToken } from "@constants/SearchParams"; -import { useNotifications } from "@hooks/NotificationsContext"; -import { useQueryParam } from "@hooks/QueryParam"; -import LoginLayout from "@layouts/LoginLayout"; -import { completeTOTPRegistrationProcess } from "@services/RegisterDevice"; - -const RegisterOneTimePassword = function () { - const { t: translate } = useTranslation(); - - const styles = useStyles(); - - const navigate = useNavigate(); - const { createSuccessNotification, createErrorNotification } = useNotifications(); - - // The secret retrieved from the API is all is ok. - const [secretURL, setSecretURL] = useState("empty"); - const [secretBase32, setSecretBase32] = useState(undefined as string | undefined); - const [hasErrored, setHasErrored] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - // Get the token from the query param to give it back to the API when requesting - // the secret for OTP. - const processToken = useQueryParam(IdentityToken); - - const handleDoneClick = () => { - navigate(IndexRoute); - }; - - const completeRegistrationProcess = useCallback(async () => { - if (!processToken) { - return; - } - - setIsLoading(true); - try { - const secret = await completeTOTPRegistrationProcess(processToken); - setSecretURL(secret.otpauth_url); - setSecretBase32(secret.base32_secret); - } catch (err) { - console.error(err); - if ((err as Error).message.includes("Request failed with status code 403")) { - createErrorNotification( - translate( - "You must open the link from the same device and browser that initiated the registration process", - ), - ); - } else { - createErrorNotification( - translate("Failed to register device, the provided link is expired or has already been used"), - ); - } - setHasErrored(true); - } - setIsLoading(false); - }, [processToken, createErrorNotification, translate]); - - useEffect(() => { - completeRegistrationProcess(); - }, [completeRegistrationProcess]); - - function SecretButton(text: string | undefined, action: string, icon: IconDefinition) { - return ( - { - navigator.clipboard.writeText(`${text}`); - createSuccessNotification(`${action}`); - }} - size="large" - > - - - ); - } - const qrcodeFuzzyStyle = isLoading || hasErrored ? styles.fuzzy : undefined; - - return ( - - - - - {translate("Need Google Authenticator?")} - - - - - - - {!hasErrored && isLoading ? : null} - {hasErrored ? : null} - - - - {secretURL !== "empty" ? ( - - ) : null} - {secretBase32 - ? SecretButton(secretBase32, translate("OTP Secret copied to clipboard"), faKey) - : null} - {secretURL !== "empty" - ? SecretButton(secretURL, translate("OTP URL copied to clipboard"), faCopy) - : null} - - - - - ); -}; - -export default RegisterOneTimePassword; - -const useStyles = makeStyles((theme: Theme) => ({ - root: { - paddingTop: theme.spacing(4), - paddingBottom: theme.spacing(4), - }, - qrcode: { - marginTop: theme.spacing(2), - marginBottom: theme.spacing(2), - padding: theme.spacing(), - backgroundColor: "white", - }, - fuzzy: { - filter: "blur(10px)", - }, - secret: { - marginTop: theme.spacing(1), - marginBottom: theme.spacing(1), - width: "256px", - }, - googleAuthenticator: {}, - googleAuthenticatorText: { - fontSize: theme.typography.fontSize * 0.8, - }, - googleAuthenticatorBadges: {}, - secretButtons: { - width: "128px", - }, - doneButton: { - width: "256px", - }, - qrcodeContainer: { - position: "relative", - display: "inline-block", - }, - loader: { - position: "absolute", - top: "calc(128px - 64px)", - left: "calc(128px - 64px)", - color: "rgba(255, 255, 255, 0.5)", - }, - failureIcon: { - position: "absolute", - top: "calc(128px - 64px)", - left: "calc(128px - 64px)", - color: red[400], - fontSize: "128px", - }, -})); diff --git a/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx b/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx index 8e9e7e939..9066049cb 100644 --- a/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx +++ b/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx @@ -6,7 +6,6 @@ import { useTranslation } from "react-i18next"; import { Route, Routes, useNavigate } from "react-router-dom"; import { - RegisterOneTimePasswordRoute, SecondFactorPushSubRoute, SecondFactorTOTPSubRoute, SecondFactorWebAuthnSubRoute, @@ -19,7 +18,6 @@ import LoginLayout from "@layouts/LoginLayout"; import { Configuration } from "@models/Configuration"; import { SecondFactorMethod } from "@models/Methods"; import { UserInfo } from "@models/UserInfo"; -import { initiateTOTPRegistrationProcess } from "@services/RegisterDevice"; import { AuthenticationLevel } from "@services/State"; import { setPreferred2FAMethod } from "@services/UserInfo"; import { isWebAuthnSupported } from "@services/WebAuthn"; @@ -42,8 +40,7 @@ const SecondFactorForm = function (props: Props) { const styles = useStyles(); const navigate = useNavigate(); const [methodSelectionOpen, setMethodSelectionOpen] = useState(false); - const { createInfoNotification, createErrorNotification } = useNotifications(); - const [registrationInProgress, setRegistrationInProgress] = useState(false); + const { createErrorNotification } = useNotifications(); const [stateWebAuthnSupported, setStateWebAuthnSupported] = useState(false); const { t: translate } = useTranslation(); @@ -51,27 +48,6 @@ const SecondFactorForm = function (props: Props) { setStateWebAuthnSupported(isWebAuthnSupported()); }, [setStateWebAuthnSupported]); - const initiateRegistration = (initiateRegistrationFunc: () => Promise, redirectRoute: string) => { - return async () => { - if (props.authenticationLevel >= AuthenticationLevel.TwoFactor) { - navigate(redirectRoute); - } else { - if (registrationInProgress) { - return; - } - setRegistrationInProgress(true); - try { - await initiateRegistrationFunc(); - createInfoNotification(translate("An email has been sent to your address to complete the process")); - } catch (err) { - console.error(err); - createErrorNotification(translate("There was a problem initiating the registration process")); - } - setRegistrationInProgress(false); - } - }; - }; - const handleMethodSelectionClick = () => { setMethodSelectionOpen(true); }; @@ -129,10 +105,9 @@ const SecondFactorForm = function (props: Props) { authenticationLevel={props.authenticationLevel} // Whether the user has a TOTP secret registered already registered={props.userInfo.has_totp} - onRegisterClick={initiateRegistration( - initiateTOTPRegistrationProcess, - RegisterOneTimePasswordRoute, - )} + onRegisterClick={() => { + navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`); + }} onSignInError={(err) => createErrorNotification(err.message)} onSignInSuccess={props.onAuthenticationSuccess} /> diff --git a/web/src/views/Settings/TwoFactorAuthentication/TOTPRegisterDialogController.tsx b/web/src/views/Settings/TwoFactorAuthentication/TOTPRegisterDialogController.tsx index c56ff20bf..fa4215c11 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/TOTPRegisterDialogController.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/TOTPRegisterDialogController.tsx @@ -107,6 +107,11 @@ export default function TOTPRegisterDialogController(props: Props) { })(); }, [totpSecretURL, props]); + const handleFinished = useCallback(() => { + props.setClosed(); + resetStates(); + }, [props]); + const handleOnClose = () => { if (!props.open) { return; @@ -191,8 +196,12 @@ export default function TOTPRegisterDialogController(props: Props) { setDialState(State.InProgress); try { - await completeTOTPRegister(dialValue); - setDialState(State.Success); + const registerValue = dialValue; + setDialValue(""); + + await completeTOTPRegister(registerValue); + + handleFinished(); } catch (err) { console.error(err); setDialState(State.Failure);