diff --git a/lib/build/emailpasswordprebuiltui.js b/lib/build/emailpasswordprebuiltui.js index d95b5c900..3ac657334 100644 --- a/lib/build/emailpasswordprebuiltui.js +++ b/lib/build/emailpasswordprebuiltui.js @@ -20,9 +20,12 @@ require("supertokens-web-js/utils/normalisedURLDomain"); require("./translationContext.js"); require("react-dom"); require("./multitenancy-shared.js"); +require("./multifactorauth-shared.js"); +require("supertokens-web-js/recipe/multifactorauth"); +require("supertokens-web-js/utils/sessionClaimValidatorStore"); +require("./recipeModule-shared.js"); require("./session-shared2.js"); require("supertokens-web-js/recipe/session"); -require("./recipeModule-shared.js"); require("./session-shared3.js"); require("./session-shared.js"); require("./translations.js"); diff --git a/lib/build/emailverificationprebuiltui.js b/lib/build/emailverificationprebuiltui.js index cd953b081..8b3493cba 100644 --- a/lib/build/emailverificationprebuiltui.js +++ b/lib/build/emailverificationprebuiltui.js @@ -25,11 +25,13 @@ require("supertokens-web-js/utils"); require("supertokens-web-js/utils/normalisedURLDomain"); require("react-dom"); require("./multitenancy-shared.js"); +require("./multifactorauth-shared.js"); +require("supertokens-web-js/recipe/multifactorauth"); +require("supertokens-web-js/utils/sessionClaimValidatorStore"); +require("./recipeModule-shared.js"); require("supertokens-web-js/recipe/session"); require("./session-shared.js"); require("supertokens-web-js/recipe/emailverification"); -require("supertokens-web-js/utils/sessionClaimValidatorStore"); -require("./recipeModule-shared.js"); function _interopDefault(e) { return e && e.__esModule ? e : { default: e }; diff --git a/lib/build/genericComponentOverrideContext.js b/lib/build/genericComponentOverrideContext.js index 5b9a429eb..6ac26e707 100644 --- a/lib/build/genericComponentOverrideContext.js +++ b/lib/build/genericComponentOverrideContext.js @@ -843,6 +843,7 @@ function getShouldUseShadowDomBasedOnBrowser(useShadowDom) { return useShadowDom !== undefined ? useShadowDom : true; } +var loginMethodTypes = ["emailpassword", "thirdparty", "passwordless"]; function normaliseMultitenancyConfig(config) { return exports.__assign(exports.__assign({}, normaliseRecipeModuleConfig(config)), { override: exports.__assign( @@ -867,7 +868,8 @@ function hasIntersectingRecipes(tenantMethods, recipeList) { return { value: true }; } }; - for (var key in tenantMethods) { + for (var _i = 0, loginMethodTypes_1 = loginMethodTypes; _i < loginMethodTypes_1.length; _i++) { + var key = loginMethodTypes_1[_i]; var state_1 = _loop_1(key); if (typeof state_1 === "object") return state_1.value; } @@ -938,7 +940,7 @@ var Multitenancy = /** @class */ (function (_super) { }; Multitenancy.getDynamicLoginMethods = function (input) { return __awaiter(this, void 0, void 0, function () { - var _a, emailPassword, passwordless, thirdParty; + var _a, emailPassword, passwordless, thirdParty, firstFactors; return __generator(this, function (_b) { switch (_b.label) { case 0: @@ -947,13 +949,15 @@ var Multitenancy = /** @class */ (function (_super) { (_a = _b.sent()), (emailPassword = _a.emailPassword), (passwordless = _a.passwordless), - (thirdParty = _a.thirdParty); + (thirdParty = _a.thirdParty), + (firstFactors = _a.firstFactors); return [ 2 /*return*/, { passwordless: passwordless, emailpassword: emailPassword, thirdparty: thirdParty, + firstFactors: firstFactors, }, ]; } diff --git a/lib/build/index.js b/lib/build/index.js index 4862636e7..7f59d9b47 100644 --- a/lib/build/index.js +++ b/lib/build/index.js @@ -17,9 +17,12 @@ require("supertokens-web-js/utils/normalisedURLPath"); require("react/jsx-runtime"); require("react-dom"); require("./multitenancy-shared.js"); +require("./multifactorauth-shared.js"); +require("supertokens-web-js/recipe/multifactorauth"); +require("supertokens-web-js/utils/sessionClaimValidatorStore"); +require("./recipeModule-shared.js"); require("./session-shared2.js"); require("supertokens-web-js/recipe/session"); -require("./recipeModule-shared.js"); exports.SuperTokensWrapper = uiEntry.SuperTokensWrapper; exports.changeLanguage = uiEntry.changeLanguage; diff --git a/lib/build/index2.js b/lib/build/index2.js index 2b9fda041..5da46d854 100644 --- a/lib/build/index2.js +++ b/lib/build/index2.js @@ -7,7 +7,8 @@ var NormalisedURLPath = require("supertokens-web-js/utils/normalisedURLPath"); var translationContext = require("./translationContext.js"); var reactDom = require("react-dom"); var componentOverrideContext = require("./multitenancy-shared.js"); -var recipe = require("./session-shared2.js"); +var recipe = require("./multifactorauth-shared.js"); +var recipe$1 = require("./session-shared2.js"); function _interopDefault(e) { return e && e.__esModule ? e : { default: e }; @@ -386,6 +387,74 @@ var DynamicLoginMethodsSpinner = function () { ); }; +// The related ADR: https://supertokens.com/docs/contribute/decisions/multitenancy/0006 +var priorityOrder = [ + { + rid: "thirdpartyemailpassword", + includes: ["thirdparty", "emailpassword"], + factorsProvided: ["thirdparty", "emailpassword"], + }, + { + rid: "thirdpartypasswordless", + includes: ["thirdparty", "passwordless"], + factorsProvided: ["thirdparty", "otp-phone", "otp-email", "link-phone", "link-email"], + }, + { rid: "emailpassword", includes: ["emailpassword"], factorsProvided: ["emailpassword"] }, + { + rid: "passwordless", + includes: ["passwordless"], + factorsProvided: ["otp-phone", "otp-email", "link-phone", "link-email"], + }, + { rid: "thirdparty", includes: ["thirdparty"], factorsProvided: ["thirdparty"] }, +]; +function chooseComponentBasedOnFirstFactors(firstFactors, routeComponents) { + var _loop_1 = function (rid, factorsProvided) { + if ( + firstFactors.length === factorsProvided.length && + factorsProvided.every(function (factor) { + return firstFactors.includes(factor); + }) + ) { + var matchingComp = routeComponents.find(function (comp) { + return comp.recipeID === rid; + }); + if (matchingComp) { + return { value: matchingComp }; + } + } + }; + // We first try to find an exact match + for (var _i = 0, priorityOrder_1 = priorityOrder; _i < priorityOrder_1.length; _i++) { + var _a = priorityOrder_1[_i], + rid = _a.rid, + factorsProvided = _a.factorsProvided; + var state_1 = _loop_1(rid, factorsProvided); + if (typeof state_1 === "object") return state_1.value; + } + var maxProvided = 0; + var component = undefined; + var _loop_2 = function (rid, factorsProvided) { + var providedByCurrent = factorsProvided.filter(function (id) { + return firstFactors.includes(id); + }).length; + if (providedByCurrent > maxProvided) { + var matchingComp = routeComponents.find(function (comp) { + return comp.recipeID === rid; + }); + if (matchingComp) { + component = matchingComp; + } + } + }; + // We find the component that provides the most factors + for (var _b = 0, _c = priorityOrder.reverse(); _b < _c.length; _b++) { + var _d = _c[_b], + rid = _d.rid, + factorsProvided = _d.factorsProvided; + _loop_2(rid, factorsProvided); + } + return component; +} var RecipeRouter = /** @class */ (function () { function RecipeRouter() { var _this = this; @@ -435,13 +504,36 @@ var RecipeRouter = /** @class */ (function () { var componentMatchingRid = routeComponents.find(function (c) { return c.matches(); }); - if (genericComponentOverrideContext.SuperTokens.usesDynamicLoginMethods === false || defaultToStaticList) { - if (routeComponents.length === 0) { - return undefined; - } else if (componentMatchingRid !== undefined) { + var defaultComp; + if (routeComponents.length === 0) { + defaultComp = undefined; + } else if (componentMatchingRid !== undefined) { + defaultComp = componentMatchingRid; + } else { + defaultComp = routeComponents[0]; + } + var matchingNonAuthComponent = routeComponents.find(function (comp) { + return !priorityOrder + .map(function (a) { + return a.rid; + }) + .includes(comp.recipeID); + }); + if (matchingNonAuthComponent) { + return matchingNonAuthComponent; + } + if (defaultToStaticList) { + return defaultComp; + } + var mfaRecipe = recipe.MultiFactorAuth.getInstance(); + if (genericComponentOverrideContext.SuperTokens.usesDynamicLoginMethods === false) { + if (componentMatchingRid) { return componentMatchingRid; + } + if (mfaRecipe) { + return chooseComponentBasedOnFirstFactors(mfaRecipe.config.getFirstFactors(), routeComponents); } else { - return routeComponents[0]; + return defaultComp; } } if (dynamicLoginMethods === undefined) { @@ -449,14 +541,6 @@ var RecipeRouter = /** @class */ (function () { "Should never come here: dynamic login methods info has not been loaded but recipeRouter rendered" ); } - // The related ADR: https://supertokens.com/docs/contribute/decisions/multitenancy/0006 - var priorityOrder = [ - { rid: "thirdpartyemailpassword", includes: ["thirdparty", "emailpassword"] }, - { rid: "thirdpartypasswordless", includes: ["thirdparty", "passwordless"] }, - { rid: "emailpassword", includes: ["emailpassword"] }, - { rid: "passwordless", includes: ["passwordless"] }, - { rid: "thirdparty", includes: ["thirdparty"] }, - ]; if ( componentMatchingRid && // if we find a component matching by rid (!priorityOrder @@ -470,20 +554,14 @@ var RecipeRouter = /** @class */ (function () { ) { return componentMatchingRid; } - var matchingNonAuthComponent = routeComponents.find(function (comp) { - return !priorityOrder - .map(function (a) { - return a.rid; - }) - .includes(comp.recipeID); - }); - if (matchingNonAuthComponent) { - return matchingNonAuthComponent; + if (dynamicLoginMethods.firstFactors !== undefined) { + return chooseComponentBasedOnFirstFactors(dynamicLoginMethods.firstFactors, routeComponents); } + // TODO: do we even need the else branch? (maybe for backwards comp.) var enabledRecipeCount = Object.keys(dynamicLoginMethods).filter(function (key) { return dynamicLoginMethods[key].enabled; }).length; - var _loop_1 = function (rid, includes) { + var _loop_3 = function (rid, includes) { if ( enabledRecipeCount === includes.length && includes.every(function (subRId) { @@ -499,14 +577,14 @@ var RecipeRouter = /** @class */ (function () { } }; // We first try to find an exact match - for (var _i = 0, priorityOrder_1 = priorityOrder; _i < priorityOrder_1.length; _i++) { - var _b = priorityOrder_1[_i], + for (var _i = 0, priorityOrder_2 = priorityOrder; _i < priorityOrder_2.length; _i++) { + var _b = priorityOrder_2[_i], rid = _b.rid, includes = _b.includes; - var state_1 = _loop_1(rid, includes); - if (typeof state_1 === "object") return state_1.value; + var state_2 = _loop_3(rid, includes); + if (typeof state_2 === "object") return state_2.value; } - var _loop_2 = function (rid, includes) { + var _loop_4 = function (rid, includes) { if ( includes.some(function (subRId) { return dynamicLoginMethods[subRId].enabled; @@ -521,12 +599,12 @@ var RecipeRouter = /** @class */ (function () { } }; // We try to find a partial match - for (var _c = 0, priorityOrder_2 = priorityOrder; _c < priorityOrder_2.length; _c++) { - var _d = priorityOrder_2[_c], + for (var _c = 0, priorityOrder_3 = priorityOrder; _c < priorityOrder_3.length; _c++) { + var _d = priorityOrder_3[_c], rid = _d.rid, includes = _d.includes; - var state_2 = _loop_2(rid, includes); - if (typeof state_2 === "object") return state_2.value; + var state_3 = _loop_4(rid, includes); + if (typeof state_3 === "object") return state_3.value; } return undefined; }; @@ -897,7 +975,7 @@ var SessionAuth = function (_a) { switch (_b.label) { case 0: if (session.current === undefined) { - session.current = recipe.Session.getInstanceOrThrow(); + session.current = recipe$1.Session.getInstanceOrThrow(); } return [ 4 /*yield*/, @@ -1029,7 +1107,7 @@ var SessionAuth = function (_a) { if (!(toSetContext.invalidClaims.length !== 0)) return [3 /*break*/, 4]; return [ 4 /*yield*/, - recipe.getFailureRedirectionInfo({ + recipe$1.getFailureRedirectionInfo({ invalidClaims: toSetContext.invalidClaims, overrideGlobalClaimValidators: props.overrideGlobalClaimValidators, userContext: userContext, @@ -1125,7 +1203,7 @@ var SessionAuth = function (_a) { if (!(props.doRedirection !== false)) return [3 /*break*/, 6]; return [ 4 /*yield*/, - recipe.getFailureRedirectionInfo({ + recipe$1.getFailureRedirectionInfo({ invalidClaims: invalidClaims, overrideGlobalClaimValidators: props.overrideGlobalClaimValidators, userContext: userContext, @@ -1209,7 +1287,7 @@ var SessionAuth = function (_a) { }); } if (session.current === undefined) { - session.current = recipe.Session.getInstanceOrThrow(); + session.current = recipe$1.Session.getInstanceOrThrow(); } if (context.loading === false) { // we return here cause addEventListener returns a function that removes diff --git a/lib/build/multifactorauth-shared.js b/lib/build/multifactorauth-shared.js index 3078bf480..21a0fff16 100644 --- a/lib/build/multifactorauth-shared.js +++ b/lib/build/multifactorauth-shared.js @@ -14,10 +14,6 @@ function _interopDefault(e) { var MultiFactorAuthWebJS__default = /*#__PURE__*/ _interopDefault(MultiFactorAuthWebJS); var NormalisedURLPath__default = /*#__PURE__*/ _interopDefault(NormalisedURLPath); -var _a = genericComponentOverrideContext.createGenericComponentsOverrideContext(), - useContext = _a[0], - Provider = _a[1]; - /* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. * * This software is licensed under the Apache License, Version 2.0 (the @@ -78,9 +74,14 @@ function normaliseMultiFactorAuthFeature(config) { ), { disableDefaultUI: disableDefaultUI, - getFirstFactors: function () { - return MultiFactorAuth.getInstanceOrThrow().getDefaultFirstFactors(); - }, + getFirstFactors: + (config === null || config === void 0 ? void 0 : config.firstFactors) !== undefined + ? function () { + return config.firstFactors; + } + : function () { + return MultiFactorAuth.getInstanceOrThrow().getDefaultFirstFactors(); + }, factorChooserScreen: (_a = config.factorChooserScreen) !== null && _a !== void 0 ? _a : {}, override: override, } @@ -110,9 +111,11 @@ var MultiFactorAuth = /** @class */ (function (_super) { var _this = _super.call(this, config) || this; _this.webJSRecipe = webJSRecipe; _this.recipeID = MultiFactorAuth.RECIPE_ID; + _this.firstFactors = []; + _this.factorRedirectionInfo = []; _this.getDefaultRedirectionURL = function (context) { return genericComponentOverrideContext.__awaiter(_this, void 0, void 0, function () { - var chooserPath; + var chooserPath, redirectInfo; return genericComponentOverrideContext.__generator(this, function (_a) { if (context.action === "FACTOR_CHOICE_REQUIRED") { chooserPath = new NormalisedURLPath__default.default(DEFAULT_FACTOR_CHOOSER_PATH); @@ -126,7 +129,13 @@ var MultiFactorAuth = /** @class */ (function (_super) { .concat(this.config.recipeId), ]; } else if (context.action === "GO_TO_FACTOR") { - // TODO + redirectInfo = this.factorRedirectionInfo.find(function (f) { + return f.id === context.factorId; + }); + if (redirectInfo !== undefined) { + return [2 /*return*/, redirectInfo.path]; + } + // TODO: access denied screen if not defined? return [2 /*return*/, "/"]; } else { return [2 /*return*/, "/"]; @@ -172,6 +181,9 @@ var MultiFactorAuth = /** @class */ (function (_super) { ), }; }; + MultiFactorAuth.getInstance = function () { + return MultiFactorAuth.instance; + }; MultiFactorAuth.getInstanceOrThrow = function () { if (MultiFactorAuth.instance === undefined) { var error = "No instance of EmailVerification found. Make sure to call the EmailVerification.init method."; @@ -184,7 +196,12 @@ var MultiFactorAuth = /** @class */ (function (_super) { return MultiFactorAuth.instance; }; MultiFactorAuth.prototype.getDefaultFirstFactors = function () { - return []; + return this.firstFactors; + }; + MultiFactorAuth.prototype.addMFAFactors = function (firstFactors, secondaryFactors) { + var _a, _b; + (_a = this.firstFactors).push.apply(_a, firstFactors); + (_b = this.factorRedirectionInfo).push.apply(_b, secondaryFactors); }; MultiFactorAuth.RECIPE_ID = "multifactorauth"; MultiFactorAuth.MultiFactorAuthClaim = MultiFactorAuthWebJS.MultiFactorAuthClaim; @@ -193,5 +210,3 @@ var MultiFactorAuth = /** @class */ (function (_super) { exports.DEFAULT_FACTOR_CHOOSER_PATH = DEFAULT_FACTOR_CHOOSER_PATH; exports.MultiFactorAuth = MultiFactorAuth; -exports.Provider = Provider; -exports.useContext = useContext; diff --git a/lib/build/multifactorauth-shared2.js b/lib/build/multifactorauth-shared2.js new file mode 100644 index 000000000..bd3b554b5 --- /dev/null +++ b/lib/build/multifactorauth-shared2.js @@ -0,0 +1,10 @@ +"use strict"; + +var genericComponentOverrideContext = require("./genericComponentOverrideContext.js"); + +var _a = genericComponentOverrideContext.createGenericComponentsOverrideContext(), + useContext = _a[0], + Provider = _a[1]; + +exports.Provider = Provider; +exports.useContext = useContext; diff --git a/lib/build/multifactorauth.js b/lib/build/multifactorauth.js index 2644efd85..e024655e5 100644 --- a/lib/build/multifactorauth.js +++ b/lib/build/multifactorauth.js @@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); var genericComponentOverrideContext = require("./genericComponentOverrideContext.js"); +var componentOverrideContext = require("./multifactorauth-shared2.js"); var recipe = require("./multifactorauth-shared.js"); require("supertokens-web-js"); require("supertokens-web-js/utils/cookieHandler"); @@ -47,7 +48,7 @@ var Wrapper = /** @class */ (function () { ); }; Wrapper.MultiFactorAuthClaim = recipe.MultiFactorAuth.MultiFactorAuthClaim; - Wrapper.ComponentsOverrideProvider = recipe.Provider; + Wrapper.ComponentsOverrideProvider = componentOverrideContext.Provider; return Wrapper; })(); var init = Wrapper.init; diff --git a/lib/build/multifactorauthprebuiltui.js b/lib/build/multifactorauthprebuiltui.js index fe5f9d1f6..7a562b90c 100644 --- a/lib/build/multifactorauthprebuiltui.js +++ b/lib/build/multifactorauthprebuiltui.js @@ -5,11 +5,12 @@ var jsxRuntime = require("react/jsx-runtime"); var NormalisedURLPath = require("supertokens-web-js/utils/normalisedURLPath"); var uiEntry = require("./index2.js"); var session = require("./session-shared3.js"); -var recipe$1 = require("./multifactorauth-shared.js"); +var componentOverrideContext = require("./multifactorauth-shared2.js"); var React = require("react"); var recipe = require("./session-shared2.js"); var translations = require("./translations.js"); var themeBase = require("./emailpassword-shared.js"); +var recipe$1 = require("./multifactorauth-shared.js"); require("supertokens-web-js"); require("supertokens-web-js/utils/cookieHandler"); require("supertokens-web-js/utils/postSuperTokensInitCallbacks"); @@ -22,9 +23,9 @@ require("react-dom"); require("./multitenancy-shared.js"); require("supertokens-web-js/recipe/session"); require("./session-shared.js"); +require("./recipeModule-shared.js"); require("supertokens-web-js/recipe/multifactorauth"); require("supertokens-web-js/utils/sessionClaimValidatorStore"); -require("./recipeModule-shared.js"); function _interopDefault(e) { return e && e.__esModule ? e : { default: e }; @@ -260,7 +261,7 @@ var MultiFactorAuthPreBuiltUI = /** @class */ (function (_super) { // Instance methods _this.getFeatures = function (useComponentOverrides) { if (useComponentOverrides === void 0) { - useComponentOverrides = recipe$1.useContext; + useComponentOverrides = componentOverrideContext.useContext; } var features = {}; if (_this.recipeInstance.config.disableDefaultUI !== true) { @@ -286,7 +287,7 @@ var MultiFactorAuthPreBuiltUI = /** @class */ (function (_super) { useComponentOverrides ) { if (useComponentOverrides === void 0) { - useComponentOverrides = recipe$1.useContext; + useComponentOverrides = componentOverrideContext.useContext; } return jsxRuntime.jsx( uiEntry.UserContextWrapper, @@ -332,13 +333,13 @@ var MultiFactorAuthPreBuiltUI = /** @class */ (function (_super) { }; MultiFactorAuthPreBuiltUI.getFeatures = function (useComponentOverrides) { if (useComponentOverrides === void 0) { - useComponentOverrides = recipe$1.useContext; + useComponentOverrides = componentOverrideContext.useContext; } return MultiFactorAuthPreBuiltUI.getInstanceOrInitAndGetInstance().getFeatures(useComponentOverrides); }; MultiFactorAuthPreBuiltUI.getFeatureComponent = function (componentName, props, useComponentOverrides) { if (useComponentOverrides === void 0) { - useComponentOverrides = recipe$1.useContext; + useComponentOverrides = componentOverrideContext.useContext; } return MultiFactorAuthPreBuiltUI.getInstanceOrInitAndGetInstance().getFeatureComponent( componentName, diff --git a/lib/build/passwordlessprebuiltui.js b/lib/build/passwordlessprebuiltui.js index 386c93baf..9fc3e1e3c 100644 --- a/lib/build/passwordlessprebuiltui.js +++ b/lib/build/passwordlessprebuiltui.js @@ -19,9 +19,12 @@ require("supertokens-web-js/utils/normalisedURLDomain"); require("./translationContext.js"); require("react-dom"); require("./multitenancy-shared.js"); +require("./multifactorauth-shared.js"); +require("supertokens-web-js/recipe/multifactorauth"); +require("supertokens-web-js/utils/sessionClaimValidatorStore"); +require("./recipeModule-shared.js"); require("./session-shared2.js"); require("supertokens-web-js/recipe/session"); -require("./recipeModule-shared.js"); require("./session-shared3.js"); require("./session-shared.js"); require("supertokens-web-js/utils/error"); diff --git a/lib/build/recipe/multifactorauth/recipe.d.ts b/lib/build/recipe/multifactorauth/recipe.d.ts index bc30eceec..1abf88682 100644 --- a/lib/build/recipe/multifactorauth/recipe.d.ts +++ b/lib/build/recipe/multifactorauth/recipe.d.ts @@ -6,6 +6,7 @@ import type { GetRedirectionURLContext, OnHandleEventContext, PreAndPostAPIHookAction, + SecondaryFactorRedirectionInfo, } from "./types"; import type { NormalisedConfigWithAppInfoAndRecipeID, RecipeInitResult, WebJSRecipeInterface } from "../../types"; export default class MultiFactorAuth extends RecipeModule< @@ -19,6 +20,8 @@ export default class MultiFactorAuth extends RecipeModule< static RECIPE_ID: string; static MultiFactorAuthClaim: import("supertokens-web-js/lib/build/recipe/multifactorauth/multiFactorAuthClaim").MultiFactorAuthClaimClass; recipeID: string; + firstFactors: string[]; + factorRedirectionInfo: SecondaryFactorRedirectionInfo[]; constructor( config: NormalisedConfigWithAppInfoAndRecipeID, webJSRecipe?: WebJSRecipeInterface @@ -26,7 +29,9 @@ export default class MultiFactorAuth extends RecipeModule< static init( config?: UserInput ): RecipeInitResult; + static getInstance(): MultiFactorAuth | undefined; static getInstanceOrThrow(): MultiFactorAuth; getDefaultRedirectionURL: (context: GetRedirectionURLContext) => Promise; getDefaultFirstFactors(): string[]; + addMFAFactors(firstFactors: string[], secondaryFactors: SecondaryFactorRedirectionInfo[]): void; } diff --git a/lib/build/recipe/multifactorauth/types.d.ts b/lib/build/recipe/multifactorauth/types.d.ts index 476044b29..12f7aefa7 100644 --- a/lib/build/recipe/multifactorauth/types.d.ts +++ b/lib/build/recipe/multifactorauth/types.d.ts @@ -24,7 +24,7 @@ export declare type UserInput = { export declare type Config = UserInput & RecipeModuleConfig; export declare type NormalisedConfig = { - getFirstFactors?: () => string[]; + getFirstFactors: () => string[]; disableDefaultUI: boolean; factorChooserScreen: FeatureBaseConfig; override: { @@ -34,9 +34,14 @@ export declare type NormalisedConfig = { ) => RecipeInterface; }; } & NormalisedRecipeModuleConfig; -export declare type GetRedirectionURLContext = { - action: "FACTOR_CHOICE_REQUIRED" | "GO_TO_FACTOR"; -}; +export declare type GetRedirectionURLContext = + | { + action: "FACTOR_CHOICE_REQUIRED"; + } + | { + action: "GO_TO_FACTOR"; + factorId: string; + }; export declare type PreAndPostAPIHookAction = "GET_MFA_INFO"; export declare type PreAPIHookContext = { action: PreAndPostAPIHookAction; @@ -52,3 +57,9 @@ export declare type FactorChooserThemeProps = { config: NormalisedConfig; userContext?: any; }; +export declare type SecondaryFactorRedirectionInfo = { + id: string; + name: string; + description: string; + path: string; +}; diff --git a/lib/build/recipe/multitenancy/types.d.ts b/lib/build/recipe/multitenancy/types.d.ts index 3ed4901a9..b65184946 100644 --- a/lib/build/recipe/multitenancy/types.d.ts +++ b/lib/build/recipe/multitenancy/types.d.ts @@ -37,6 +37,7 @@ export declare type GetLoginMethodsResponseNormalized = { name: string; }[]; }; + firstFactors: string[]; }; export declare type ComponentOverrideMap = { MultitenancyDynamicLoginMethodsSpinnerTheme_Override?: ComponentOverride; diff --git a/lib/build/session.js b/lib/build/session.js index 53f86d87f..c109ea077 100644 --- a/lib/build/session.js +++ b/lib/build/session.js @@ -22,6 +22,9 @@ require("./recipeModule-shared.js"); require("./translationContext.js"); require("react-dom"); require("./multitenancy-shared.js"); +require("./multifactorauth-shared.js"); +require("supertokens-web-js/recipe/multifactorauth"); +require("supertokens-web-js/utils/sessionClaimValidatorStore"); exports.BooleanClaim = session.BooleanClaim; exports.PrimitiveArrayClaim = session.PrimitiveArrayClaim; diff --git a/lib/build/sessionprebuiltui.js b/lib/build/sessionprebuiltui.js index 6aab3f72e..229b076ee 100644 --- a/lib/build/sessionprebuiltui.js +++ b/lib/build/sessionprebuiltui.js @@ -19,8 +19,11 @@ require("supertokens-web-js/utils/normalisedURLDomain"); require("supertokens-web-js/utils/normalisedURLPath"); require("react-dom"); require("./multitenancy-shared.js"); -require("supertokens-web-js/recipe/session"); +require("./multifactorauth-shared.js"); +require("supertokens-web-js/recipe/multifactorauth"); +require("supertokens-web-js/utils/sessionClaimValidatorStore"); require("./recipeModule-shared.js"); +require("supertokens-web-js/recipe/session"); function ErrorRoundIcon() { return jsxRuntime.jsxs( diff --git a/lib/build/thirdpartyemailpasswordprebuiltui.js b/lib/build/thirdpartyemailpasswordprebuiltui.js index b2c554c94..81d1dc42f 100644 --- a/lib/build/thirdpartyemailpasswordprebuiltui.js +++ b/lib/build/thirdpartyemailpasswordprebuiltui.js @@ -21,9 +21,12 @@ require("supertokens-web-js/utils"); require("supertokens-web-js/utils/normalisedURLDomain"); require("react-dom"); require("./multitenancy-shared.js"); +require("./multifactorauth-shared.js"); +require("supertokens-web-js/recipe/multifactorauth"); +require("supertokens-web-js/utils/sessionClaimValidatorStore"); +require("./recipeModule-shared.js"); require("./session-shared2.js"); require("supertokens-web-js/recipe/session"); -require("./recipeModule-shared.js"); require("./session-shared3.js"); require("./session-shared.js"); require("./emailpassword-shared4.js"); diff --git a/lib/build/thirdpartypasswordlessprebuiltui.js b/lib/build/thirdpartypasswordlessprebuiltui.js index f855a7556..7a42a4b29 100644 --- a/lib/build/thirdpartypasswordlessprebuiltui.js +++ b/lib/build/thirdpartypasswordlessprebuiltui.js @@ -21,9 +21,12 @@ require("supertokens-web-js/utils"); require("supertokens-web-js/utils/normalisedURLDomain"); require("react-dom"); require("./multitenancy-shared.js"); +require("./multifactorauth-shared.js"); +require("supertokens-web-js/recipe/multifactorauth"); +require("supertokens-web-js/utils/sessionClaimValidatorStore"); +require("./recipeModule-shared.js"); require("./session-shared2.js"); require("supertokens-web-js/recipe/session"); -require("./recipeModule-shared.js"); require("./session-shared3.js"); require("./session-shared.js"); require("./passwordless-shared.js"); diff --git a/lib/build/thirdpartyprebuiltui.js b/lib/build/thirdpartyprebuiltui.js index 79ddc4d7d..dca4df3b9 100644 --- a/lib/build/thirdpartyprebuiltui.js +++ b/lib/build/thirdpartyprebuiltui.js @@ -18,9 +18,12 @@ require("supertokens-web-js/utils/normalisedURLDomain"); require("./translationContext.js"); require("react-dom"); require("./multitenancy-shared.js"); +require("./multifactorauth-shared.js"); +require("supertokens-web-js/recipe/multifactorauth"); +require("supertokens-web-js/utils/sessionClaimValidatorStore"); +require("./recipeModule-shared.js"); require("./session-shared2.js"); require("supertokens-web-js/recipe/session"); -require("./recipeModule-shared.js"); require("./session-shared3.js"); require("./session-shared.js"); require("supertokens-web-js/recipe/thirdparty"); diff --git a/lib/build/ui-entry.js b/lib/build/ui-entry.js index da116aaec..5a2e95d9f 100644 --- a/lib/build/ui-entry.js +++ b/lib/build/ui-entry.js @@ -10,13 +10,16 @@ require("supertokens-web-js/utils/normalisedURLPath"); require("./translationContext.js"); require("react-dom"); require("./multitenancy-shared.js"); +require("./multifactorauth-shared.js"); +require("supertokens-web-js/recipe/multifactorauth"); +require("supertokens-web-js/utils/postSuperTokensInitCallbacks"); +require("supertokens-web-js/utils/sessionClaimValidatorStore"); +require("./recipeModule-shared.js"); require("./session-shared2.js"); require("supertokens-web-js/recipe/session"); -require("./recipeModule-shared.js"); require("supertokens-web-js/utils"); require("supertokens-web-js"); require("supertokens-web-js/utils/cookieHandler"); -require("supertokens-web-js/utils/postSuperTokensInitCallbacks"); require("supertokens-web-js/utils/windowHandler"); require("supertokens-web-js/recipe/multitenancy"); require("supertokens-web-js/utils/normalisedURLDomain"); diff --git a/lib/ts/recipe/multifactorauth/recipe.tsx b/lib/ts/recipe/multifactorauth/recipe.tsx index ace54711d..e6a71bb36 100644 --- a/lib/ts/recipe/multifactorauth/recipe.tsx +++ b/lib/ts/recipe/multifactorauth/recipe.tsx @@ -36,6 +36,7 @@ import type { GetRedirectionURLContext, OnHandleEventContext, PreAndPostAPIHookAction, + SecondaryFactorRedirectionInfo, } from "./types"; import type { NormalisedConfigWithAppInfoAndRecipeID, RecipeInitResult, WebJSRecipeInterface } from "../../types"; import type { NormalisedAppInfo } from "../../types"; @@ -52,6 +53,8 @@ export default class MultiFactorAuth extends RecipeModule< static MultiFactorAuthClaim = MultiFactorAuthClaim; public recipeID = MultiFactorAuth.RECIPE_ID; + public firstFactors: string[] = []; + public factorRedirectionInfo: SecondaryFactorRedirectionInfo[] = []; constructor( config: NormalisedConfigWithAppInfoAndRecipeID, @@ -102,6 +105,10 @@ export default class MultiFactorAuth extends RecipeModule< }; } + static getInstance(): MultiFactorAuth | undefined { + return MultiFactorAuth.instance; + } + static getInstanceOrThrow(): MultiFactorAuth { if (MultiFactorAuth.instance === undefined) { let error = "No instance of EmailVerification found. Make sure to call the EmailVerification.init method."; @@ -123,7 +130,11 @@ export default class MultiFactorAuth extends RecipeModule< this.config.recipeId }`; } else if (context.action === "GO_TO_FACTOR") { - // TODO + const redirectInfo = this.factorRedirectionInfo.find((f) => f.id === context.factorId); + if (redirectInfo !== undefined) { + return redirectInfo.path; + } + // TODO: access denied screen if not defined? return "/"; } else { return "/"; @@ -131,6 +142,11 @@ export default class MultiFactorAuth extends RecipeModule< }; getDefaultFirstFactors(): string[] { - return []; + return this.firstFactors; + } + + addMFAFactors(firstFactors: string[], secondaryFactors: SecondaryFactorRedirectionInfo[]) { + this.firstFactors.push(...firstFactors); + this.factorRedirectionInfo.push(...secondaryFactors); } } diff --git a/lib/ts/recipe/multifactorauth/types.ts b/lib/ts/recipe/multifactorauth/types.ts index 897a698d6..3386a5eb7 100644 --- a/lib/ts/recipe/multifactorauth/types.ts +++ b/lib/ts/recipe/multifactorauth/types.ts @@ -45,7 +45,7 @@ export type Config = UserInput & RecipeModuleConfig; export type NormalisedConfig = { - getFirstFactors?: () => string[]; + getFirstFactors: () => string[]; disableDefaultUI: boolean; factorChooserScreen: FeatureBaseConfig; @@ -57,9 +57,14 @@ export type NormalisedConfig = { }; } & NormalisedRecipeModuleConfig; -export type GetRedirectionURLContext = { - action: "FACTOR_CHOICE_REQUIRED" | "GO_TO_FACTOR"; -}; +export type GetRedirectionURLContext = + | { + action: "FACTOR_CHOICE_REQUIRED"; + } + | { + action: "GO_TO_FACTOR"; + factorId: string; + }; export type PreAndPostAPIHookAction = "GET_MFA_INFO"; @@ -79,3 +84,10 @@ export type FactorChooserThemeProps = { config: NormalisedConfig; userContext?: any; }; + +export type SecondaryFactorRedirectionInfo = { + id: string; + name: string; + description: string; + path: string; +}; diff --git a/lib/ts/recipe/multifactorauth/utils.ts b/lib/ts/recipe/multifactorauth/utils.ts index 10fa202da..8a1d9e39e 100644 --- a/lib/ts/recipe/multifactorauth/utils.ts +++ b/lib/ts/recipe/multifactorauth/utils.ts @@ -34,7 +34,10 @@ export function normaliseMultiFactorAuthFeature(config?: Config): NormalisedConf return { ...normaliseRecipeModuleConfig(config), disableDefaultUI, - getFirstFactors: () => MultiFactorAuth.getInstanceOrThrow().getDefaultFirstFactors(), + getFirstFactors: + config?.firstFactors !== undefined + ? () => config!.firstFactors! + : () => MultiFactorAuth.getInstanceOrThrow().getDefaultFirstFactors(), factorChooserScreen: config.factorChooserScreen ?? {}, override, }; diff --git a/lib/ts/recipe/multitenancy/recipe.ts b/lib/ts/recipe/multitenancy/recipe.ts index cf6daf3eb..240cba59f 100644 --- a/lib/ts/recipe/multitenancy/recipe.ts +++ b/lib/ts/recipe/multitenancy/recipe.ts @@ -77,12 +77,15 @@ export default class Multitenancy extends BaseRecipeModule { static async getDynamicLoginMethods( input: Parameters[0] ): Promise { - const { emailPassword, passwordless, thirdParty } = await MultitenancyWebJS.getLoginMethods(input); + const { emailPassword, passwordless, thirdParty, firstFactors } = await MultitenancyWebJS.getLoginMethods( + input + ); return { passwordless: passwordless, emailpassword: emailPassword, thirdparty: thirdParty, + firstFactors, }; } diff --git a/lib/ts/recipe/multitenancy/types.ts b/lib/ts/recipe/multitenancy/types.ts index 32a373b89..37e99a15c 100644 --- a/lib/ts/recipe/multitenancy/types.ts +++ b/lib/ts/recipe/multitenancy/types.ts @@ -41,6 +41,7 @@ export type GetLoginMethodsResponseNormalized = { name: string; }[]; }; + firstFactors: string[]; }; export type ComponentOverrideMap = { diff --git a/lib/ts/recipe/multitenancy/utils.ts b/lib/ts/recipe/multitenancy/utils.ts index 8b6310bff..5693b41e1 100644 --- a/lib/ts/recipe/multitenancy/utils.ts +++ b/lib/ts/recipe/multitenancy/utils.ts @@ -3,6 +3,8 @@ import { normaliseRecipeModuleConfig } from "../recipeModule/utils"; import type { UserInput, NormalisedConfig, GetLoginMethodsResponseNormalized } from "./types"; import type { BaseRecipeModule } from "../recipeModule/baseRecipeModule"; +const loginMethodTypes = ["emailpassword", "thirdparty", "passwordless"] as const; + export function normaliseMultitenancyConfig(config?: UserInput): NormalisedConfig { return { ...normaliseRecipeModuleConfig(config), @@ -17,9 +19,9 @@ export function hasIntersectingRecipes( tenantMethods: GetLoginMethodsResponseNormalized, recipeList: BaseRecipeModule[] ): boolean { - for (const key in tenantMethods) { + for (const key of loginMethodTypes) { const hasIntersection = recipeList.some((recipe) => { - if (tenantMethods[key as keyof GetLoginMethodsResponseNormalized].enabled) { + if (tenantMethods[key].enabled) { return recipe.recipeID === key || recipe.recipeID.includes(key); } return false; diff --git a/lib/ts/recipe/recipeRouter/index.tsx b/lib/ts/recipe/recipeRouter/index.tsx index 612439c51..36f1a6d6b 100644 --- a/lib/ts/recipe/recipeRouter/index.tsx +++ b/lib/ts/recipe/recipeRouter/index.tsx @@ -1,4 +1,5 @@ import SuperTokens from "../../superTokens"; +import MultiFactorAuth from "../multifactorauth/recipe"; import type { RecipeFeatureComponentMap } from "../../types"; import type { BaseFeatureComponentMap, ComponentWithRecipeAndMatchingMethod } from "../../types"; @@ -6,6 +7,64 @@ import type { GetLoginMethodsResponseNormalized } from "../multitenancy/types"; import type RecipeModule from "../recipeModule"; import type NormalisedURLPath from "supertokens-web-js/lib/build/normalisedURLPath"; +// The related ADR: https://supertokens.com/docs/contribute/decisions/multitenancy/0006 +const priorityOrder: { + rid: string; + includes: ("thirdparty" | "emailpassword" | "passwordless")[]; + factorsProvided: string[]; +}[] = [ + { + rid: "thirdpartyemailpassword", + includes: ["thirdparty", "emailpassword"], + factorsProvided: ["thirdparty", "emailpassword"], + }, + { + rid: "thirdpartypasswordless", + includes: ["thirdparty", "passwordless"], + factorsProvided: ["thirdparty", "otp-phone", "otp-email", "link-phone", "link-email"], + }, + { rid: "emailpassword", includes: ["emailpassword"], factorsProvided: ["emailpassword"] }, + { + rid: "passwordless", + includes: ["passwordless"], + factorsProvided: ["otp-phone", "otp-email", "link-phone", "link-email"], + }, + { rid: "thirdparty", includes: ["thirdparty"], factorsProvided: ["thirdparty"] }, +]; + +function chooseComponentBasedOnFirstFactors( + firstFactors: string[], + routeComponents: ComponentWithRecipeAndMatchingMethod[] +) { + // We first try to find an exact match + for (const { rid, factorsProvided } of priorityOrder) { + if ( + firstFactors.length === factorsProvided.length && + factorsProvided.every((factor) => firstFactors.includes(factor)) + ) { + const matchingComp = routeComponents.find((comp) => comp.recipeID === rid); + if (matchingComp) { + return matchingComp; + } + } + } + + const maxProvided = 0; + let component = undefined; + // We find the component that provides the most factors + for (const { rid, factorsProvided } of priorityOrder.reverse()) { + const providedByCurrent = factorsProvided.filter((id) => firstFactors.includes(id)).length; + if (providedByCurrent > maxProvided) { + const matchingComp = routeComponents.find((comp) => comp.recipeID === rid); + if (matchingComp) { + component = matchingComp; + } + } + } + + return component; +} + export abstract class RecipeRouter { private pathsToFeatureComponentWithRecipeIdMap?: BaseFeatureComponentMap; public abstract recipeInstance: RecipeModule; @@ -31,13 +90,38 @@ export abstract class RecipeRouter { }, [] as ComponentWithRecipeAndMatchingMethod[]); const componentMatchingRid = routeComponents.find((c) => c.matches()); - if (SuperTokens.usesDynamicLoginMethods === false || defaultToStaticList) { - if (routeComponents.length === 0) { - return undefined; - } else if (componentMatchingRid !== undefined) { + + let defaultComp; + if (routeComponents.length === 0) { + defaultComp = undefined; + } else if (componentMatchingRid !== undefined) { + defaultComp = componentMatchingRid; + } else { + defaultComp = routeComponents[0]; + } + + const matchingNonAuthComponent = routeComponents.find( + (comp) => !priorityOrder.map((a) => a.rid).includes(comp.recipeID) + ); + + if (matchingNonAuthComponent) { + return matchingNonAuthComponent; + } + + if (defaultToStaticList) { + return defaultComp; + } + + const mfaRecipe = MultiFactorAuth.getInstance(); + if (SuperTokens.usesDynamicLoginMethods === false) { + if (componentMatchingRid) { return componentMatchingRid; + } + + if (mfaRecipe) { + return chooseComponentBasedOnFirstFactors(mfaRecipe.config.getFirstFactors(), routeComponents); } else { - return routeComponents[0]; + return defaultComp; } } @@ -47,32 +131,20 @@ export abstract class RecipeRouter { ); } - // The related ADR: https://supertokens.com/docs/contribute/decisions/multitenancy/0006 - const priorityOrder: { rid: string; includes: (keyof GetLoginMethodsResponseNormalized)[] }[] = [ - { rid: "thirdpartyemailpassword", includes: ["thirdparty", "emailpassword"] }, - { rid: "thirdpartypasswordless", includes: ["thirdparty", "passwordless"] }, - { rid: "emailpassword", includes: ["emailpassword"] }, - { rid: "passwordless", includes: ["passwordless"] }, - { rid: "thirdparty", includes: ["thirdparty"] }, - ]; - if ( componentMatchingRid && // if we find a component matching by rid (!priorityOrder.map((a) => a.rid).includes(componentMatchingRid.recipeID) || // from a non-auth recipe - dynamicLoginMethods[componentMatchingRid.recipeID as keyof GetLoginMethodsResponseNormalized] + dynamicLoginMethods[componentMatchingRid.recipeID as "passwordless" | "thirdparty" | "emailpassword"] ?.enabled === true) // or an enabled auth recipe ) { return componentMatchingRid; } - const matchingNonAuthComponent = routeComponents.find( - (comp) => !priorityOrder.map((a) => a.rid).includes(comp.recipeID) - ); - - if (matchingNonAuthComponent) { - return matchingNonAuthComponent; + if (dynamicLoginMethods.firstFactors !== undefined) { + return chooseComponentBasedOnFirstFactors(dynamicLoginMethods.firstFactors, routeComponents); } + // TODO: do we even need the else branch? (maybe for backwards comp.) const enabledRecipeCount = Object.keys(dynamicLoginMethods).filter( (key) => (dynamicLoginMethods as any)[key].enabled ).length; diff --git a/package-lock.json b/package-lock.json index 1aef3c02e..5fa17dd82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16244,7 +16244,7 @@ }, "node_modules/supertokens-web-js": { "version": "0.8.0", - "resolved": "git+ssh://git@github.com/supertokens/supertokens-web-js.git#e0035834475c5bae41295bd97d3e06cbf138c867", + "resolved": "git+ssh://git@github.com/supertokens/supertokens-web-js.git#f6aa50d8b92dff7d86221ecc03eb1cc808ac2273", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -29414,7 +29414,7 @@ "integrity": "sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg==" }, "supertokens-web-js": { - "version": "git+ssh://git@github.com/supertokens/supertokens-web-js.git#e0035834475c5bae41295bd97d3e06cbf138c867", + "version": "git+ssh://git@github.com/supertokens/supertokens-web-js.git#f6aa50d8b92dff7d86221ecc03eb1cc808ac2273", "from": "supertokens-web-js@github:supertokens/supertokens-web-js#feat/mfa", "peer": true, "requires": {