Skip to content

Commit

Permalink
Merge branch 'feat/mfa/recipe-setup' into feat/mfa/otp
Browse files Browse the repository at this point in the history
  • Loading branch information
porcellus committed Nov 9, 2023
2 parents fb89c18 + 1990358 commit 666d3fb
Show file tree
Hide file tree
Showing 29 changed files with 119 additions and 67 deletions.
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

## [0.36.0] - 2023-11-XX

### Overview

#### Introducing MFA

With this release, we are introducing MultiFactorAuth and TOTP, this will let you:

- require (2FA or MFA) during sign in
- make use of our TOTP

Check our [guide](https://supertokens.com/docs/thirdpartyemailpassword/common-customizations/multi-factor-auth/overview) for more information.

To use this you'll need compatible versions:

- Core>=8.0.0
- supertokens-node>=17.0.0 (support is pending in other backend SDKs)
- supertokens-website>=17.0.3
- supertokens-web-js>=0.9.0
- supertokens-auth-react>=0.36.0

### Changes

- Added support for FDI 1.19 (Node SDK>= 17.0.0), but keeping support FDI version 1.17 and 1.18 (node >= 15.0.0, golang>=0.13, python>=0.15.0)
- Added `firstFactors` into the return type of `getLoginMethods`
- Added the `MultiFactorAuth` recipe
- Updated how we select which login UI to show to take the `firstFactors` config value into account (defined in the `MultiFactorAuth` recipe or in the tenant information)
- Refactored/renamed some styling options (`resetPasswordHeaderTitle` -> `headerTitle withBackButton`)
- Added a `useShadowDom` prop to the `AccessDeniedScreen`
- Added an `error` prop to the `AccessDeniedScreen` that can be used to describe the reason access is denied.

## [0.35.5] - 2023-10-06

### Changes
Expand Down
4 changes: 4 additions & 0 deletions examples/for-tests-react-16/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ const formFields = [
const testContext = getTestContext();

let recipeList = [
MultiFactorAuth.init({
firstFactors: testContext.firstFactors,
}),
Multitenancy.init({
override: {
functions: (oI) => ({
Expand Down Expand Up @@ -491,6 +494,7 @@ export function DashboardHelper({ redirectOnLogout, ...props } = {}) {
</div>
<div className="session-context-userId">session context userID: {sessionContext.userId}</div>
<pre className="invalidClaims">{JSON.stringify(sessionContext.invalidClaims, undefined, 2)}</pre>
<a onClick={() => MultiFactorAuth.redirectToFactorChooser(true, props.history)}>MFA chooser</a>
</div>
);
}
Expand Down
3 changes: 2 additions & 1 deletion examples/for-tests-react-16/src/AppWithReactDomRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import { EmailVerificationPreBuiltUI } from "supertokens-auth-react/recipe/email
import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui";
import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui";
import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui";
import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui";
import { AccessDeniedScreen } from "supertokens-auth-react/recipe/session/prebuiltui";
import { getEnabledRecipes } from "./testContext";

function AppWithReactDomRouter(props) {
const enabledRecipes = getEnabledRecipes();
const emailVerificationMode = window.localStorage.getItem("mode") || "OFF";

let recipePreBuiltUIList = [];
let recipePreBuiltUIList = [MultiFactorAuthPreBuiltUI];
if (enabledRecipes.includes("emailpassword")) {
recipePreBuiltUIList.push(EmailPasswordPreBuiltUI);
}
Expand Down
1 change: 1 addition & 0 deletions examples/for-tests-react-16/src/testContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function getTestContext() {
staticProviderList: localStorage.getItem("staticProviderList"),
mockTenantId: localStorage.getItem("mockTenantId"),
clientType: localStorage.getItem("clientType") || undefined,
firstFactors: localStorage.getItem("firstFactors")?.split(", "),
};
return ret;
}
Expand Down
3 changes: 0 additions & 3 deletions examples/for-tests/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -485,9 +485,6 @@ export function DashboardHelper({ redirectOnLogout, ...props } = {}) {
<div className="session-context-userId">session context userID: {sessionContext.userId}</div>
<pre className="invalidClaims">{JSON.stringify(sessionContext.invalidClaims, undefined, 2)}</pre>
<a onClick={() => MultiFactorAuth.redirectToFactorChooser(true, props.history)}>MFA chooser</a>
<a onClick={() => MultiFactorAuth.redirectToFactor("totp", true, props.history)}>TOTP</a>
<a onClick={() => MultiFactorAuth.redirectToFactor("otp-email", true, props.history)}>OTP-Email</a>
<a onClick={() => MultiFactorAuth.redirectToFactor("otp-phone", true, props.history)}>OTP-Phone</a>
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion examples/for-tests/src/testContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function getEnabledRecipes() {

let enabledRecipes = [];

if (true) {
if (testContext.usesDynamicLoginMethods) {
if (testContext.clientRecipeListForDynamicLogin) {
enabledRecipes = JSON.parse(testContext.clientRecipeListForDynamicLogin);
} else {
Expand Down
2 changes: 1 addition & 1 deletion frontendDriverInterfaceSupported.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"_comment": "contains a list of frontend-backend interface versions that this package supports",
"versions": ["1.17", "1.18"]
"versions": ["1.17", "1.18", "1.19"]
}
2 changes: 1 addition & 1 deletion lib/build/genericComponentOverrideContext.js

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

2 changes: 1 addition & 1 deletion lib/build/index2.js

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

2 changes: 1 addition & 1 deletion lib/build/multifactorauth-shared.js

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

6 changes: 2 additions & 4 deletions lib/build/passwordless-shared2.js

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

8 changes: 2 additions & 6 deletions lib/build/recipe/multifactorauth/types.d.ts

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

2 changes: 1 addition & 1 deletion lib/build/recipe/multitenancy/types.d.ts

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

5 changes: 2 additions & 3 deletions lib/build/recipe/passwordless/recipe.d.ts

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

17 changes: 9 additions & 8 deletions lib/build/thirdpartyemailpasswordprebuiltui.js

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

14 changes: 9 additions & 5 deletions lib/build/thirdpartypasswordlessprebuiltui.js

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

2 changes: 1 addition & 1 deletion lib/build/version.d.ts

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

4 changes: 3 additions & 1 deletion lib/ts/recipe/multifactorauth/recipe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ export default class MultiFactorAuth extends RecipeModule<
} else if (context.action === "GO_TO_FACTOR") {
const redirectInfo = this.getSecondaryFactors().find((f) => f.id === context.factorId);
if (redirectInfo !== undefined) {
return this.config.appInfo.websiteBasePath.appendPath(redirectInfo.path).getAsStringDangerous();
return this.config.appInfo.websiteBasePath
.appendPath(new NormalisedURLPath(redirectInfo.path))
.getAsStringDangerous();
}
throw new Error("Requested redirect to unknown factor id");
} else {
Expand Down
8 changes: 2 additions & 6 deletions lib/ts/recipe/multifactorauth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import type {
} from "../recipeModule/types";
import type { FC } from "react";
import type { OverrideableBuilder } from "supertokens-js-override";
import type NormalisedURLPath from "supertokens-web-js/lib/build/normalisedURLPath";
import type { RecipeInterface } from "supertokens-web-js/recipe/multifactorauth";
import type { MFAFactorInfo } from "supertokens-web-js/recipe/multifactorauth/types";

Expand Down Expand Up @@ -81,10 +80,7 @@ export type PreAPIHookContext = {
userContext: any;
};

export type OnHandleEventContext = {
action: "FACTOR_CHOSEN";
userContext: any;
};
export type OnHandleEventContext = never;

export type FactorChooserThemeProps = {
mfaInfo: MFAFactorInfo;
Expand All @@ -102,5 +98,5 @@ export type SecondaryFactorRedirectionInfo = {
name: string;
description: string;
logo: FC;
path: NormalisedURLPath;
path: string;
};
2 changes: 1 addition & 1 deletion lib/ts/recipe/multitenancy/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type GetLoginMethodsResponseNormalized = {
name: string;
}[];
};
firstFactors: string[];
firstFactors?: string[];
};

export type ComponentOverrideMap = {
Expand Down
5 changes: 2 additions & 3 deletions lib/ts/recipe/passwordless/recipe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
*/

import PasswordlessWebJS from "supertokens-web-js/recipe/passwordless";
import NormalisedURLPath from "supertokens-web-js/utils/normalisedURLPath";
import { PostSuperTokensInitCallbacks } from "supertokens-web-js/utils/postSuperTokensInitCallbacks";

import { OTPIcon } from "../../components/assets/otpIcon";
Expand All @@ -45,14 +44,14 @@ export const otpPhoneFactor = {
id: "otp-phone",
name: "SMS based OTP",
description: "Get an OTP code on your phone to complete the authentication request",
path: new NormalisedURLPath("/mfa/otp-phone"),
path: "/mfa/otp-phone",
logo: OTPIcon,
};
export const otpEmailFactor = {
id: "otp-email",
name: "Email based OTP",
description: "Get an OTP code on your email address to complete the authentication request",
path: new NormalisedURLPath("/mfa/otp-email"),
path: "/mfa/otp-email",
logo: OTPIcon,
};

Expand Down
2 changes: 1 addition & 1 deletion lib/ts/recipe/recipeRouter/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export abstract class RecipeRouter {
return chooseComponentBasedOnFirstFactors(dynamicLoginMethods.firstFactors, routeComponents);
}

// TODO: do we even need the else branch? (maybe for backwards comp.)
// We may get here if the app is using an older BE that doesn't support MFA
const enabledRecipeCount = Object.keys(dynamicLoginMethods).filter(
(key) => (dynamicLoginMethods as any)[key].enabled
).length;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,18 @@ const SignInAndUpTheme: React.FC<ThirdPartyEmailPasswordSignInAndUpThemeProps> =
let emailPasswordEnabled: boolean = props.emailPasswordRecipe !== undefined;

if (usesDynamicLoginMethods) {
thirdPartyEnabled = thirdPartyEnabled && loginMethods!.firstFactors.includes("thirdparty") && hasProviders;
thirdPartyEnabled =
(loginMethods!.firstFactors === undefined
? loginMethods!.thirdparty.enabled
: loginMethods!.firstFactors.includes("thirdparty")) && hasProviders;
emailPasswordEnabled =
emailPasswordEnabled &&
loginMethods!.firstFactors.includes("emailpassword") &&
props.emailPasswordRecipe !== undefined;
(loginMethods!.firstFactors === undefined
? loginMethods!.emailpassword.enabled
: loginMethods!.firstFactors.includes("emailpassword")) && props.emailPasswordRecipe !== undefined;
} else if (mfa !== undefined) {
thirdPartyEnabled = thirdPartyEnabled && mfa.isFirstFactorEnabledOnClient("thirdparty") && hasProviders;
thirdPartyEnabled = mfa.isFirstFactorEnabledOnClient("thirdparty") && hasProviders;
emailPasswordEnabled =
emailPasswordEnabled &&
mfa.isFirstFactorEnabledOnClient("emailpassword") &&
props.emailPasswordRecipe !== undefined;
mfa.isFirstFactorEnabledOnClient("emailpassword") && props.emailPasswordRecipe !== undefined;
}

if (thirdPartyEnabled === false && emailPasswordEnabled === false) {
Expand Down
Loading

0 comments on commit 666d3fb

Please sign in to comment.