Skip to content

Commit

Permalink
feat: finishing totp initial impl
Browse files Browse the repository at this point in the history
  • Loading branch information
porcellus committed Oct 30, 2023
1 parent 3dd7c52 commit f17de3a
Show file tree
Hide file tree
Showing 21 changed files with 510 additions and 161 deletions.
11 changes: 8 additions & 3 deletions lib/build/recipe/totp/components/themes/mfa/blockedScreen.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions lib/build/recipe/totp/components/themes/mfa/retryButton.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions lib/build/recipe/totp/components/themes/translations.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions lib/build/recipe/totp/types.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

365 changes: 260 additions & 105 deletions lib/build/totpprebuiltui.js

Large diffs are not rendered by default.

27 changes: 22 additions & 5 deletions lib/ts/recipe/totp/components/features/mfa/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import { Fragment } from "react";
import { useMemo } from "react";
import { WindowHandlerReference } from "supertokens-web-js/utils/windowHandler";

import { redirectToAuth } from "../../../../..";
import { ComponentOverrideContext } from "../../../../../components/componentOverride/componentOverrideContext";
import FeatureWrapper from "../../../../../components/featureWrapper";
import { useUserContext } from "../../../../../usercontext";
import { getQueryParams, getRedirectToPathFromURL, useOnMountAPICall } from "../../../../../utils";
import { MultiFactorAuthClaim } from "../../../../multifactorauth";
import MultiFactorAuth from "../../../../multifactorauth/recipe";
import SessionRecipe from "../../../../session/recipe";
import MFATOTPThemeWrapper from "../../themes/mfa";
Expand All @@ -50,14 +52,16 @@ export const useFeatureReducer = (): [TOTPMFAState, React.Dispatch<TOTPMFAAction
loaded: true,
error: action.error,
deviceInfo: action.deviceInfo,
showBackButton: action.showBackButton,
isBlocked: false,
showSecret: false,
};
case "setBlocked":
return {
...oldState,
isBlocked: true,
nextRetryAt: action.nextRetryAt,
error: action.error,
deviceInfo: undefined,
};
case "setError":
return {
Expand Down Expand Up @@ -96,6 +100,7 @@ export const useFeatureReducer = (): [TOTPMFAState, React.Dispatch<TOTPMFAAction
deviceInfo: undefined,
showSecret: false,
isBlocked: false,
showBackButton: false,
},
(initArg) => {
let error: string | undefined = undefined;
Expand Down Expand Up @@ -152,6 +157,7 @@ function useOnLoad(recipeImpl: RecipeInterface, dispatch: React.Dispatch<TOTPMFA
return;
}
if (doSetup && !isAllowedToSetup) {
// TODO: redirect to access denied
dispatch({ type: "setError", error: "Setup not allowed" });
return;
}
Expand All @@ -166,9 +172,15 @@ function useOnLoad(recipeImpl: RecipeInterface, dispatch: React.Dispatch<TOTPMFA
};
delete (deviceInfo as any).status;
}
const mfaClaim = await SessionRecipe.getInstanceOrThrow().getClaimValue({
claim: MultiFactorAuthClaim,
userContext,
});
const nextLength = mfaClaim?.n.length ?? 0;
const showBackButton = nextLength !== 1; // If we have finished logging in or if the factorChooser is available

// No need to check if the component is unmounting, since this has no effect then.
dispatch({ type: "load", deviceInfo, error });
dispatch({ type: "load", deviceInfo, error, showBackButton });
},
[dispatch, recipeImpl, userContext]
);
Expand Down Expand Up @@ -207,9 +219,16 @@ export function useChildProps(
// If we reach this code this means we are using react-router-dom v6
return history(-1);
},
onRetryClicked() {
onRetryClicked: () => {
dispatch({ type: "restartFlow", error: undefined });
},
onSignOutClicked: async () => {
await SessionRecipe.getInstanceOrThrow().signOut({ userContext });
if (state.deviceInfo) {
await recipeImplementation.removeDevice({ deviceName: state.deviceInfo.deviceName, userContext });
}
await redirectToAuth({ redirectBack: false, history: history });
},
onSuccess: () => {
const redirectToPath = getRedirectToPathFromURL();
const redirectInfo =
Expand Down Expand Up @@ -335,8 +354,6 @@ function getModifiedRecipeImplementation(
error: "ERROR_TOTP_MFA_VERIFY_DEVICE_BLOCKED",
nextRetryAt: Date.now() + res.retryAfterMs,
});
} else if (res.status === "INVALID_TOTP_ERROR") {
dispatch({ type: "setError", error: "ERROR_TOTP_MFA_VERIFY_DEVICE_INVALID_TOTP" });
} else if (res.status === "UNKNOWN_DEVICE_ERROR") {
await originalImpl.clearDeviceInfo({ userContext: input.userContext });
dispatch({ type: "restartFlow", error: "ERROR_TOTP_MFA_VERIFY_DEVICE_UNKNOWN_DEVICE" });
Expand Down
9 changes: 7 additions & 2 deletions lib/ts/recipe/totp/components/themes/mfa/blockedScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,23 @@
import { BlockedIcon } from "../../../../../components/assets/blockedIcon";
import { withOverride } from "../../../../../components/componentOverride/withOverride";
import { useTranslation } from "../../../../../translation/translationContext";
import { FormRow } from "../../../../emailpassword/components/library";

const TOTPBlockedScreen: React.FC = () => {
import { RetryButton } from "./retryButton";

const TOTPBlockedScreen: React.FC<{ nextRetryAt: number; onRetry: () => void }> = (props) => {
const t = useTranslation();

return (
<div data-supertokens="container">
<div data-supertokens="row noFormRow">
<BlockedIcon />
<div data-supertokens="headerTitle">{t("TOTP_BLOCKED_TITLE")}</div>
<div data-supertokens="divider" />
<div data-supertokens="headerSubtitle secondaryText">{t("TOTP_BLOCKED_SUBTITLE")}</div>
<div data-supertokens="divider" />
<FormRow key="form-button">
<RetryButton nextRetryAt={props.nextRetryAt} onClick={props.onRetry} />
</FormRow>
</div>
</div>
);
Expand Down
23 changes: 17 additions & 6 deletions lib/ts/recipe/totp/components/themes/mfa/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const SignInUpTheme: React.FC<TOTPMFAProps & { activeScreen: TOTPMFAScreens }> =
};

return activeScreen === TOTPMFAScreens.Blocked ? (
<BlockedScreen />
<BlockedScreen nextRetryAt={featureState.nextRetryAt!} onRetry={props.onRetryClicked} />
) : activeScreen === TOTPMFAScreens.Loading ? (
<LoadingScreen />
) : (
Expand All @@ -67,11 +67,18 @@ const SignInUpTheme: React.FC<TOTPMFAProps & { activeScreen: TOTPMFAScreens }> =
{featureState.loaded && (
<React.Fragment>
{activeScreen === TOTPMFAScreens.DeviceSetup ? (
<DeviceSetupHeader {...commonProps} />
<DeviceSetupHeader
{...commonProps}
showBackButton={featureState.showBackButton}
onBackButtonClicked={props.onBackButtonClicked}
/>
) : (
<CodeVerificationHeader {...commonProps} />
<CodeVerificationHeader
{...commonProps}
showBackButton={featureState.showBackButton}
onBackButtonClicked={props.onBackButtonClicked}
/>
)}
{featureState.error !== undefined && <GeneralError error={featureState.error} />}
{activeScreen === TOTPMFAScreens.DeviceSetup && (
<DeviceInfoSection
{...commonProps}
Expand All @@ -80,14 +87,18 @@ const SignInUpTheme: React.FC<TOTPMFAProps & { activeScreen: TOTPMFAScreens }> =
onShowSecretClick={props.onShowSecretClick}
/>
)}
{featureState.error !== undefined && <GeneralError error={featureState.error} />}
<CodeForm
{...commonProps}
onSuccess={props.onSuccess}
footer={
activeScreen === TOTPMFAScreens.DeviceSetup ? (
<DeviceSetupFooter {...commonProps} />
<DeviceSetupFooter {...commonProps} onSignOutClicked={props.onSignOutClicked} />
) : (
<CodeVerificationFooter {...commonProps} />
<CodeVerificationFooter
{...commonProps}
onSignOutClicked={props.onSignOutClicked}
/>
)
}
/>
Expand Down
84 changes: 84 additions & 0 deletions lib/ts/recipe/totp/components/themes/mfa/retryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

/*
* Imports.
*/

import React, { useCallback, useEffect, useState } from "react";

import { withOverride } from "../../../../../components/componentOverride/withOverride";
import { useTranslation } from "../../../../../translation/translationContext";

export const RetryButton = withOverride(
"TOTPRetryButton",
function TOTPRetryButton({
nextRetryAt,
onClick,
}: {
nextRetryAt: number;
onClick: () => void;
}): JSX.Element | null {
const t = useTranslation();

const getTimeLeft = useCallback(() => {
const timeLeft = nextRetryAt - Date.now();
return timeLeft < 0 ? undefined : Math.ceil(timeLeft / 1000);
}, [nextRetryAt]);

const [secsUntilRetry, setSecsUntilRetry] = useState<number | undefined>(getTimeLeft());

useEffect(() => {
// This runs every time the loginAttemptInfo updates, so after every resend
const interval = setInterval(() => {
const timeLeft = getTimeLeft();

if (timeLeft === undefined) {
clearInterval(interval);
}

setSecsUntilRetry(timeLeft);
}, 500);

return () => {
// This can safely run twice
clearInterval(interval);
};
}, [getTimeLeft, setSecsUntilRetry]);

return (
<button
type="button"
disabled={secsUntilRetry !== undefined}
onClick={onClick}
data-supertokens="button retryCodeBtn">
{secsUntilRetry !== undefined ? (
<React.Fragment>
{t("TOTP_MFA_BLOCKED_TIMER_START")}
<strong>
{Math.floor(secsUntilRetry / 60)
.toString()
.padStart(2, "0")}
:{(secsUntilRetry % 60).toString().padStart(2, "0")}
</strong>
{t("TOTP_MFA_BLOCKED_TIMER_END")}
</React.Fragment>
) : (
t("TOTP_MFA_BLOCKED_RETRY")
)}
</button>
);
}
);
7 changes: 5 additions & 2 deletions lib/ts/recipe/totp/components/themes/mfa/totpCodeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const CodeForm = withOverride(
});
}

// We can redirect these statuses, since they all cause a redirection
// We can return these statuses, since they all cause a redirection
// and we don't really want to show anything
if (
response.status === "OK" ||
Expand All @@ -87,7 +87,10 @@ export const CodeForm = withOverride(
}

if (response.status === "INVALID_TOTP_ERROR") {
throw new STGeneralError("GENERAL_ERROR_OTP_INVALID");
return {
status: "FIELD_ERROR",
formFields: [{ id: "totp", error: "INVALID_TOTP_ERROR" }],
};
}

throw new STGeneralError("SOMETHING_WENT_WRONG_ERROR");
Expand Down
Loading

0 comments on commit f17de3a

Please sign in to comment.