From f3c955e6564e73f7a5abc82571eefc61bc628ec8 Mon Sep 17 00:00:00 2001 From: Mihaly Lengyel Date: Mon, 16 Oct 2023 11:27:54 +0200 Subject: [PATCH] feat: navigation based on claim validation failures --- examples/for-tests/src/App.js | 4 + examples/for-tests/src/testContext.js | 2 +- lib/build/emailverification-shared.js | 32 ++--- lib/build/index2.js | 19 --- lib/build/multifactorauth-shared.js | 111 +++++++++++++++++- lib/build/passwordless-shared2.js | 18 +++ lib/build/passwordless.js | 3 + .../emailVerificationClaim.d.ts | 2 +- lib/build/recipe/emailverification/index.d.ts | 4 +- .../recipe/emailverification/recipe.d.ts | 2 +- lib/build/recipe/multifactorauth/index.d.ts | 10 +- .../multifactorauth/multiFactorAuthClaim.d.ts | 48 ++++++++ lib/build/recipe/multifactorauth/recipe.d.ts | 3 +- lib/build/thirdpartypasswordless.js | 3 + .../emailVerificationClaim.ts | 4 +- lib/ts/recipe/emailverification/recipe.tsx | 2 +- lib/ts/recipe/multifactorauth/index.ts | 6 +- .../multifactorauth/multiFactorAuthClaim.ts | 92 +++++++++++++++ lib/ts/recipe/multifactorauth/recipe.tsx | 7 +- lib/ts/recipe/passwordless/recipe.tsx | 19 +++ test/server/index.js | 12 ++ 21 files changed, 344 insertions(+), 59 deletions(-) rename lib/build/{claims => recipe/emailverification}/emailVerificationClaim.d.ts (86%) create mode 100644 lib/build/recipe/multifactorauth/multiFactorAuthClaim.d.ts rename lib/ts/{claims => recipe/emailverification}/emailVerificationClaim.ts (92%) create mode 100644 lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts diff --git a/examples/for-tests/src/App.js b/examples/for-tests/src/App.js index f65dac002..6273474b2 100644 --- a/examples/for-tests/src/App.js +++ b/examples/for-tests/src/App.js @@ -14,6 +14,7 @@ import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; import ThirdPartyEmailPassword from "supertokens-auth-react/recipe/thirdpartyemailpassword"; import ThirdPartyPasswordless from "supertokens-auth-react/recipe/thirdpartypasswordless"; import UserRoles from "supertokens-auth-react/recipe/userroles"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import axios from "axios"; import { useSessionContext } from "supertokens-auth-react/recipe/session"; @@ -171,6 +172,9 @@ const formFields = [ const testContext = getTestContext(); let recipeList = [ + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + }), Multitenancy.init({ override: { functions: (oI) => ({ diff --git a/examples/for-tests/src/testContext.js b/examples/for-tests/src/testContext.js index 618e990ba..ab1e6e3c5 100644 --- a/examples/for-tests/src/testContext.js +++ b/examples/for-tests/src/testContext.js @@ -23,7 +23,7 @@ export function getEnabledRecipes() { let enabledRecipes = []; - if (testContext.usesDynamicLoginMethods) { + if (true) { if (testContext.clientRecipeListForDynamicLogin) { enabledRecipes = JSON.parse(testContext.clientRecipeListForDynamicLogin); } else { diff --git a/lib/build/emailverification-shared.js b/lib/build/emailverification-shared.js index f025cfe3e..d6a0e2799 100644 --- a/lib/build/emailverification-shared.js +++ b/lib/build/emailverification-shared.js @@ -18,6 +18,22 @@ 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 + * "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. + */ +var DEFAULT_VERIFY_EMAIL_PATH = "/verify-email"; + var EmailVerificationClaimClass = /** @class */ (function (_super) { genericComponentOverrideContext.__extends(EmailVerificationClaimClass, _super); function EmailVerificationClaimClass(getRecipeImpl, onFailureRedirection) { @@ -57,22 +73,6 @@ var EmailVerificationClaimClass = /** @class */ (function (_super) { return EmailVerificationClaimClass; })(EmailVerificationWebJS.EmailVerificationClaimClass); -/* 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. - */ -var DEFAULT_VERIFY_EMAIL_PATH = "/verify-email"; - var getFunctionOverrides = function (onHandleEvent) { return function (originalImp) { return genericComponentOverrideContext.__assign(genericComponentOverrideContext.__assign({}, originalImp), { diff --git a/lib/build/index2.js b/lib/build/index2.js index 75c54626c..0b24c118d 100644 --- a/lib/build/index2.js +++ b/lib/build/index2.js @@ -408,8 +408,6 @@ var priorityOrder = [ { rid: "thirdparty", includes: ["thirdparty"], factorsProvided: ["thirdparty"] }, ]; function chooseComponentBasedOnFirstFactors(firstFactors, routeComponents) { - console.log("chooseComponentBasedOnFirstFactors"); - console.log({ firstFactors: firstFactors }); var _loop_1 = function (rid, factorsProvided) { if ( firstFactors.length === factorsProvided.length && @@ -420,7 +418,6 @@ function chooseComponentBasedOnFirstFactors(firstFactors, routeComponents) { var matchingComp = routeComponents.find(function (comp) { return comp.recipeID === rid; }); - console.log("exact match", rid, !!matchingComp); if (matchingComp) { return { value: matchingComp }; } @@ -440,18 +437,10 @@ function chooseComponentBasedOnFirstFactors(firstFactors, routeComponents) { var providedByCurrent = factorsProvided.filter(function (id) { return firstFactors.includes(id); }).length; - console.log({ - rid: rid, - providedByCurrent: factorsProvided.filter(function (id) { - return firstFactors.includes(id); - }), - c: providedByCurrent, - }); if (providedByCurrent >= maxProvided) { var matchingComp = routeComponents.find(function (comp) { return comp.recipeID === rid; }); - console.log("new max", rid); if (matchingComp) { maxProvided = providedByCurrent; component = matchingComp; @@ -531,11 +520,6 @@ var RecipeRouter = /** @class */ (function () { }) .includes(comp.recipeID); }); - console.log({ - defaultToStaticList: defaultToStaticList, - matchingNonAuthComponent: !!matchingNonAuthComponent, - componentMatchingRid: !!componentMatchingRid, - }); if (matchingNonAuthComponent) { return matchingNonAuthComponent; } @@ -544,15 +528,12 @@ var RecipeRouter = /** @class */ (function () { } var mfaRecipe = recipe.MultiFactorAuth.getInstance(); if (genericComponentOverrideContext.SuperTokens.usesDynamicLoginMethods === false) { - console.log("usesDynamicLoginMethods false"); if (componentMatchingRid) { return componentMatchingRid; } if (mfaRecipe) { - console.log("mfa enabled"); return chooseComponentBasedOnFirstFactors(mfaRecipe.config.getFirstFactors(), routeComponents); } else { - console.log("returning default"); return defaultComp; } } diff --git a/lib/build/multifactorauth-shared.js b/lib/build/multifactorauth-shared.js index 21a0fff16..4bec6918c 100644 --- a/lib/build/multifactorauth-shared.js +++ b/lib/build/multifactorauth-shared.js @@ -39,6 +39,98 @@ var getFunctionOverrides = function ( }; }; +var MultiFactorAuthClaimClass = /** @class */ (function () { + function MultiFactorAuthClaimClass(getRecipeImpl, getRedirectURL, onFailureRedirection) { + var _this = this; + this.webJSClaim = new MultiFactorAuthWebJS.MultiFactorAuthClaimClass(getRecipeImpl); + this.refresh = this.webJSClaim.refresh; + this.getLastFetchedTime = this.webJSClaim.getLastFetchedTime; + this.getValueFromPayload = this.webJSClaim.getValueFromPayload; + this.id = this.webJSClaim.id; + var defaultOnFailureRedirection = function (_a) { + var reason = _a.reason, + userContext = _a.userContext; + if (reason.nextFactorOptions) { + if (reason.nextFactorOptions.length === 1) { + return getRedirectURL( + { action: "GO_TO_FACTOR", factorId: reason.nextFactorOptions[0] }, + userContext + ); + } else { + return getRedirectURL({ action: "FACTOR_CHOICE_REQUIRED" }, userContext); + } + } + return getRedirectURL({ action: "GO_TO_FACTOR", factorId: reason.factorId }, userContext); + }; + this.validators = genericComponentOverrideContext.__assign( + genericComponentOverrideContext.__assign({}, this.webJSClaim.validators), + { + hasCompletedDefaultFactors: function (doRedirection, showAccessDeniedOnFailure) { + if (doRedirection === void 0) { + doRedirection = true; + } + if (showAccessDeniedOnFailure === void 0) { + showAccessDeniedOnFailure = true; + } + var orig = _this.webJSClaim.validators.hasCompletedDefaultFactors(); + return genericComponentOverrideContext.__assign( + genericComponentOverrideContext.__assign({}, orig), + { + showAccessDeniedOnFailure: showAccessDeniedOnFailure, + onFailureRedirection: + onFailureRedirection !== null && onFailureRedirection !== void 0 + ? onFailureRedirection + : function ( + _a // TODO: feels brittle to rely on reason + ) { + var reason = _a.reason, + userContext = _a.userContext; + return doRedirection + ? defaultOnFailureRedirection({ + reason: reason, + userContext: userContext, + }) + : undefined; + }, + } + ); + }, + hasCompletedFactors: function (requirements, doRedirection, showAccessDeniedOnFailure) { + if (doRedirection === void 0) { + doRedirection = true; + } + if (showAccessDeniedOnFailure === void 0) { + showAccessDeniedOnFailure = true; + } + var orig = _this.webJSClaim.validators.hasCompletedFactors(requirements); + return genericComponentOverrideContext.__assign( + genericComponentOverrideContext.__assign({}, orig), + { + showAccessDeniedOnFailure: showAccessDeniedOnFailure, + onFailureRedirection: + onFailureRedirection !== null && onFailureRedirection !== void 0 + ? onFailureRedirection + : function ( + _a // TODO: feels brittle to rely on reason + ) { + var reason = _a.reason, + userContext = _a.userContext; + return doRedirection + ? defaultOnFailureRedirection({ + reason: reason, + userContext: userContext, + }) + : undefined; + }, + } + ); + }, + } + ); + } + return MultiFactorAuthClaimClass; +})(); + /* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. * * This software is licensed under the Apache License, Version 2.0 (the @@ -116,7 +208,7 @@ var MultiFactorAuth = /** @class */ (function (_super) { _this.getDefaultRedirectionURL = function (context) { return genericComponentOverrideContext.__awaiter(_this, void 0, void 0, function () { var chooserPath, redirectInfo; - return genericComponentOverrideContext.__generator(this, function (_a) { + return genericComponentOverrideContext.__generator(this, function (_b) { if (context.action === "FACTOR_CHOICE_REQUIRED") { chooserPath = new NormalisedURLPath__default.default(DEFAULT_FACTOR_CHOOSER_PATH); return [ @@ -199,12 +291,21 @@ var MultiFactorAuth = /** @class */ (function (_super) { 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); + var _b, _c; + (_b = this.firstFactors).push.apply(_b, firstFactors); + (_c = this.factorRedirectionInfo).push.apply(_c, secondaryFactors); }; + var _a; + _a = MultiFactorAuth; MultiFactorAuth.RECIPE_ID = "multifactorauth"; - MultiFactorAuth.MultiFactorAuthClaim = MultiFactorAuthWebJS.MultiFactorAuthClaim; + MultiFactorAuth.MultiFactorAuthClaim = new MultiFactorAuthClaimClass( + function () { + return MultiFactorAuth.getInstanceOrThrow().webJSRecipe; + }, + function (context) { + return _a.getInstanceOrThrow().getDefaultRedirectionURL(context); + } + ); return MultiFactorAuth; })(index.RecipeModule); diff --git a/lib/build/passwordless-shared2.js b/lib/build/passwordless-shared2.js index 06f1fa598..addc013cf 100644 --- a/lib/build/passwordless-shared2.js +++ b/lib/build/passwordless-shared2.js @@ -2,7 +2,9 @@ var genericComponentOverrideContext = require("./genericComponentOverrideContext.js"); var PasswordlessWebJS = require("supertokens-web-js/recipe/passwordless"); +var postSuperTokensInitCallbacks = require("supertokens-web-js/utils/postSuperTokensInitCallbacks"); var utils = require("./authRecipe-shared.js"); +var recipe = require("./multifactorauth-shared.js"); var windowHandler = require("supertokens-web-js/utils/windowHandler"); function _interopDefault(e) { @@ -493,6 +495,22 @@ var Passwordless = /** @class */ (function (_super) { } Passwordless.init = function (config) { var normalisedConfig = normalisePasswordlessConfig(config); + postSuperTokensInitCallbacks.PostSuperTokensInitCallbacks.addPostInitCallback(function () { + var mfa = recipe.MultiFactorAuth.getInstance(); + if (mfa !== undefined) { + mfa.addMFAFactors( + ["otp-phone", "otp-email", "link-phone", "link-email"], + [ + { + id: "otp-phone", + name: "Phone-based OTP", + description: "OTP delivered by a text message", + path: "/check-auth/otp", + }, + ] + ); + } + }); return { recipeID: Passwordless.RECIPE_ID, authReact: function (appInfo) { diff --git a/lib/build/passwordless.js b/lib/build/passwordless.js index f8cba3862..ce950e342 100644 --- a/lib/build/passwordless.js +++ b/lib/build/passwordless.js @@ -20,6 +20,9 @@ require("./authRecipe-shared.js"); require("./recipeModule-shared.js"); require("./session-shared2.js"); require("supertokens-web-js/recipe/session"); +require("./multifactorauth-shared.js"); +require("supertokens-web-js/recipe/multifactorauth"); +require("supertokens-web-js/utils/sessionClaimValidatorStore"); /* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. * diff --git a/lib/build/claims/emailVerificationClaim.d.ts b/lib/build/recipe/emailverification/emailVerificationClaim.d.ts similarity index 86% rename from lib/build/claims/emailVerificationClaim.d.ts rename to lib/build/recipe/emailverification/emailVerificationClaim.d.ts index 1308ba5a1..732815830 100644 --- a/lib/build/claims/emailVerificationClaim.d.ts +++ b/lib/build/recipe/emailverification/emailVerificationClaim.d.ts @@ -1,5 +1,5 @@ import { EmailVerificationClaimClass as EmailVerificationClaimClassWebJS } from "supertokens-web-js/recipe/emailverification"; -import type { ValidationFailureCallback } from "../types"; +import type { ValidationFailureCallback } from "../../types"; import type { RecipeInterface } from "supertokens-web-js/recipe/emailverification"; export declare class EmailVerificationClaimClass extends EmailVerificationClaimClassWebJS { constructor(getRecipeImpl: () => RecipeInterface, onFailureRedirection?: ValidationFailureCallback); diff --git a/lib/build/recipe/emailverification/index.d.ts b/lib/build/recipe/emailverification/index.d.ts index 4a0c471e9..f03efd525 100644 --- a/lib/build/recipe/emailverification/index.d.ts +++ b/lib/build/recipe/emailverification/index.d.ts @@ -4,7 +4,7 @@ import { GetRedirectionURLContext, PreAPIHookContext, OnHandleEventContext } fro import { UserInput } from "./types"; import type { RecipeFunctionOptions } from "supertokens-web-js/recipe/emailverification"; export default class Wrapper { - static EmailVerificationClaim: import("../../claims/emailVerificationClaim").EmailVerificationClaimClass; + static EmailVerificationClaim: import("./emailVerificationClaim").EmailVerificationClaimClass; static init( config?: UserInput ): import("../../types").RecipeInitResult< @@ -43,7 +43,7 @@ declare const EmailVerificationComponentsOverrideProvider: import("react").FC< components: import("./types").ComponentOverrideMap; }> >; -declare const EmailVerificationClaim: import("../../claims/emailVerificationClaim").EmailVerificationClaimClass; +declare const EmailVerificationClaim: import("./emailVerificationClaim").EmailVerificationClaimClass; export { init, isEmailVerified, diff --git a/lib/build/recipe/emailverification/recipe.d.ts b/lib/build/recipe/emailverification/recipe.d.ts index bac2db2ea..5944564d7 100644 --- a/lib/build/recipe/emailverification/recipe.d.ts +++ b/lib/build/recipe/emailverification/recipe.d.ts @@ -1,6 +1,6 @@ import EmailVerificationWebJS from "supertokens-web-js/recipe/emailverification"; -import { EmailVerificationClaimClass } from "../../claims/emailVerificationClaim"; import RecipeModule from "../recipeModule"; +import { EmailVerificationClaimClass } from "./emailVerificationClaim"; import type { UserInput, NormalisedConfig, diff --git a/lib/build/recipe/multifactorauth/index.d.ts b/lib/build/recipe/multifactorauth/index.d.ts index 4794457c1..3b28cd3c9 100644 --- a/lib/build/recipe/multifactorauth/index.d.ts +++ b/lib/build/recipe/multifactorauth/index.d.ts @@ -1,11 +1,11 @@ /// -import { RecipeInterface } from "supertokens-web-js/recipe/emailverification"; +import { RecipeInterface } from "supertokens-web-js/recipe/multifactorauth"; import { GetRedirectionURLContext, PreAPIHookContext, OnHandleEventContext } from "./types"; import { UserInput } from "./types"; -import type { MFAFactorInfo } from "supertokens-web-js/lib/build/recipe/multifactorauth/types"; -import type { RecipeFunctionOptions } from "supertokens-web-js/recipe/emailverification"; +import type { RecipeFunctionOptions } from "supertokens-web-js/recipe/multifactorauth"; +import type { MFAFactorInfo } from "supertokens-web-js/recipe/multifactorauth/types"; export default class Wrapper { - static MultiFactorAuthClaim: import("supertokens-web-js/lib/build/recipe/multifactorauth/multiFactorAuthClaim").MultiFactorAuthClaimClass; + static MultiFactorAuthClaim: import("./multiFactorAuthClaim").MultiFactorAuthClaimClass; static init( config?: UserInput ): import("../../types").RecipeInitResult< @@ -32,7 +32,7 @@ declare const MultiFactorAuthComponentsOverrideProvider: import("react").FC< components: import("./types").ComponentOverrideMap; }> >; -declare const MultiFactorAuthClaim: import("supertokens-web-js/lib/build/recipe/multifactorauth/multiFactorAuthClaim").MultiFactorAuthClaimClass; +declare const MultiFactorAuthClaim: import("./multiFactorAuthClaim").MultiFactorAuthClaimClass; export { init, getMFAInfo, diff --git a/lib/build/recipe/multifactorauth/multiFactorAuthClaim.d.ts b/lib/build/recipe/multifactorauth/multiFactorAuthClaim.d.ts new file mode 100644 index 000000000..7caac34d3 --- /dev/null +++ b/lib/build/recipe/multifactorauth/multiFactorAuthClaim.d.ts @@ -0,0 +1,48 @@ +import { MultiFactorAuthClaimClass as MultiFactorAuthClaimClassWebJS } from "supertokens-web-js/recipe/multifactorauth"; +import type { SessionClaimValidator, ValidationFailureCallback } from "../../types"; +import type { RecipeInterface } from "supertokens-web-js/recipe/multifactorauth"; +import type { MFARequirementList } from "supertokens-web-js/recipe/multifactorauth/types"; +export declare class MultiFactorAuthClaimClass { + private webJSClaim; + readonly id: string; + readonly refresh: (userContext: any) => Promise; + readonly getLastFetchedTime: (payload: any, _userContext?: any) => number | undefined; + readonly getValueFromPayload: ( + payload: any, + _userContext?: any + ) => + | { + c: Record; + n: string[]; + } + | undefined; + validators: Omit< + MultiFactorAuthClaimClassWebJS["validators"], + "hasCompletedDefaultFactors" | "hasCompletedFactors" + > & { + hasCompletedDefaultFactors: ( + doRedirection?: boolean, + showAccessDeniedOnFailure?: boolean + ) => SessionClaimValidator; + hasCompletedFactors: ( + requirements: MFARequirementList, + doRedirection?: boolean, + showAccessDeniedOnFailure?: boolean + ) => SessionClaimValidator; + }; + constructor( + getRecipeImpl: () => RecipeInterface, + getRedirectURL: ( + context: + | { + action: "GO_TO_FACTOR"; + factorId: string; + } + | { + action: "FACTOR_CHOICE_REQUIRED"; + }, + userContext: any + ) => Promise, + onFailureRedirection?: ValidationFailureCallback + ); +} diff --git a/lib/build/recipe/multifactorauth/recipe.d.ts b/lib/build/recipe/multifactorauth/recipe.d.ts index 1abf88682..c4aa11fe1 100644 --- a/lib/build/recipe/multifactorauth/recipe.d.ts +++ b/lib/build/recipe/multifactorauth/recipe.d.ts @@ -1,5 +1,6 @@ import MultiFactorAuthWebJS from "supertokens-web-js/recipe/multifactorauth"; import RecipeModule from "../recipeModule"; +import { MultiFactorAuthClaimClass } from "./multiFactorAuthClaim"; import type { UserInput, NormalisedConfig, @@ -18,7 +19,7 @@ export default class MultiFactorAuth extends RecipeModule< readonly webJSRecipe: WebJSRecipeInterface; static instance?: MultiFactorAuth; static RECIPE_ID: string; - static MultiFactorAuthClaim: import("supertokens-web-js/lib/build/recipe/multifactorauth/multiFactorAuthClaim").MultiFactorAuthClaimClass; + static MultiFactorAuthClaim: MultiFactorAuthClaimClass; recipeID: string; firstFactors: string[]; factorRedirectionInfo: SecondaryFactorRedirectionInfo[]; diff --git a/lib/build/thirdpartypasswordless.js b/lib/build/thirdpartypasswordless.js index 7de9631c9..796cbe40c 100644 --- a/lib/build/thirdpartypasswordless.js +++ b/lib/build/thirdpartypasswordless.js @@ -26,6 +26,9 @@ require("supertokens-web-js/lib/build/normalisedURLPath"); require("supertokens-web-js/recipe/thirdpartypasswordless"); require("./passwordless-shared2.js"); require("supertokens-web-js/recipe/passwordless"); +require("./multifactorauth-shared.js"); +require("supertokens-web-js/recipe/multifactorauth"); +require("supertokens-web-js/utils/sessionClaimValidatorStore"); var Wrapper = /** @class */ (function () { function Wrapper() {} diff --git a/lib/ts/claims/emailVerificationClaim.ts b/lib/ts/recipe/emailverification/emailVerificationClaim.ts similarity index 92% rename from lib/ts/claims/emailVerificationClaim.ts rename to lib/ts/recipe/emailverification/emailVerificationClaim.ts index ec735c115..a1929fb26 100644 --- a/lib/ts/claims/emailVerificationClaim.ts +++ b/lib/ts/recipe/emailverification/emailVerificationClaim.ts @@ -1,8 +1,8 @@ import { EmailVerificationClaimClass as EmailVerificationClaimClassWebJS } from "supertokens-web-js/recipe/emailverification"; -import EmailVerification from "../recipe/emailverification/recipe"; +import EmailVerification from "./recipe"; -import type { ValidationFailureCallback } from "../types"; +import type { ValidationFailureCallback } from "../../types"; import type { RecipeInterface } from "supertokens-web-js/recipe/emailverification"; export class EmailVerificationClaimClass extends EmailVerificationClaimClassWebJS { diff --git a/lib/ts/recipe/emailverification/recipe.tsx b/lib/ts/recipe/emailverification/recipe.tsx index 8d2acc62e..411f31e78 100644 --- a/lib/ts/recipe/emailverification/recipe.tsx +++ b/lib/ts/recipe/emailverification/recipe.tsx @@ -22,11 +22,11 @@ import NormalisedURLPath from "supertokens-web-js/utils/normalisedURLPath"; import { PostSuperTokensInitCallbacks } from "supertokens-web-js/utils/postSuperTokensInitCallbacks"; import { SessionClaimValidatorStore } from "supertokens-web-js/utils/sessionClaimValidatorStore"; -import { EmailVerificationClaimClass } from "../../claims/emailVerificationClaim"; import { SSR_ERROR } from "../../constants"; import RecipeModule from "../recipeModule"; import { DEFAULT_VERIFY_EMAIL_PATH } from "./constants"; +import { EmailVerificationClaimClass } from "./emailVerificationClaim"; import { getFunctionOverrides } from "./functionOverrides"; import { normaliseEmailVerificationFeature } from "./utils"; diff --git a/lib/ts/recipe/multifactorauth/index.ts b/lib/ts/recipe/multifactorauth/index.ts index 572c17f70..5161e7145 100644 --- a/lib/ts/recipe/multifactorauth/index.ts +++ b/lib/ts/recipe/multifactorauth/index.ts @@ -16,7 +16,7 @@ /* * Imports. */ -import { RecipeInterface } from "supertokens-web-js/recipe/emailverification"; +import { RecipeInterface } from "supertokens-web-js/recipe/multifactorauth"; import { getNormalisedUserContext } from "../../utils"; @@ -25,8 +25,8 @@ import MultiFactorAuthRecipe from "./recipe"; import { GetRedirectionURLContext, PreAPIHookContext, OnHandleEventContext } from "./types"; import { UserInput } from "./types"; -import type { MFAFactorInfo } from "supertokens-web-js/lib/build/recipe/multifactorauth/types"; -import type { RecipeFunctionOptions } from "supertokens-web-js/recipe/emailverification"; +import type { RecipeFunctionOptions } from "supertokens-web-js/recipe/multifactorauth"; +import type { MFAFactorInfo } from "supertokens-web-js/recipe/multifactorauth/types"; export default class Wrapper { static MultiFactorAuthClaim = MultiFactorAuthRecipe.MultiFactorAuthClaim; diff --git a/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts b/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts new file mode 100644 index 000000000..4e9428f10 --- /dev/null +++ b/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts @@ -0,0 +1,92 @@ +import { MultiFactorAuthClaimClass as MultiFactorAuthClaimClassWebJS } from "supertokens-web-js/recipe/multifactorauth"; + +import type { SessionClaimValidator, ValidationFailureCallback } from "../../types"; +import type { RecipeInterface } from "supertokens-web-js/recipe/multifactorauth"; +import type { MFARequirementList } from "supertokens-web-js/recipe/multifactorauth/types"; + +export class MultiFactorAuthClaimClass { + private webJSClaim: MultiFactorAuthClaimClassWebJS; + public readonly id: string; + public readonly refresh: (userContext: any) => Promise; + public readonly getLastFetchedTime: (payload: any, _userContext?: any) => number | undefined; + public readonly getValueFromPayload: ( + payload: any, + _userContext?: any + ) => { c: Record; n: string[] } | undefined; + public validators: Omit< + MultiFactorAuthClaimClassWebJS["validators"], + "hasCompletedDefaultFactors" | "hasCompletedFactors" + > & { + hasCompletedDefaultFactors: ( + doRedirection?: boolean, + showAccessDeniedOnFailure?: boolean + ) => SessionClaimValidator; + hasCompletedFactors: ( + requirements: MFARequirementList, + doRedirection?: boolean, + showAccessDeniedOnFailure?: boolean + ) => SessionClaimValidator; + }; + + constructor( + getRecipeImpl: () => RecipeInterface, + getRedirectURL: ( + context: { action: "GO_TO_FACTOR"; factorId: string } | { action: "FACTOR_CHOICE_REQUIRED" }, + userContext: any + ) => Promise, + onFailureRedirection?: ValidationFailureCallback + ) { + this.webJSClaim = new MultiFactorAuthClaimClassWebJS(getRecipeImpl); + this.refresh = this.webJSClaim.refresh; + this.getLastFetchedTime = this.webJSClaim.getLastFetchedTime; + this.getValueFromPayload = this.webJSClaim.getValueFromPayload; + this.id = this.webJSClaim.id; + + const defaultOnFailureRedirection = ({ reason, userContext }: any) => { + if (reason.nextFactorOptions) { + if (reason.nextFactorOptions.length === 1) { + return getRedirectURL( + { action: "GO_TO_FACTOR", factorId: reason.nextFactorOptions[0] }, + userContext + ); + } else { + return getRedirectURL({ action: "FACTOR_CHOICE_REQUIRED" }, userContext); + } + } + return getRedirectURL({ action: "GO_TO_FACTOR", factorId: reason.factorId }, userContext); + }; + + this.validators = { + ...this.webJSClaim.validators, + hasCompletedDefaultFactors: (doRedirection = true, showAccessDeniedOnFailure = true) => { + const orig = this.webJSClaim.validators.hasCompletedDefaultFactors(); + return { + ...orig, + showAccessDeniedOnFailure, + onFailureRedirection: + onFailureRedirection ?? + (( + { reason, userContext } // TODO: feels brittle to rely on reason + ) => (doRedirection ? defaultOnFailureRedirection({ reason, userContext }) : undefined)), + }; + }, + + hasCompletedFactors: ( + requirements: MFARequirementList, + doRedirection = true, + showAccessDeniedOnFailure = true + ) => { + const orig = this.webJSClaim.validators.hasCompletedFactors(requirements); + return { + ...orig, + showAccessDeniedOnFailure, + onFailureRedirection: + onFailureRedirection ?? + (( + { reason, userContext } // TODO: feels brittle to rely on reason + ) => (doRedirection ? defaultOnFailureRedirection({ reason, userContext }) : undefined)), + }; + }, + }; + } +} diff --git a/lib/ts/recipe/multifactorauth/recipe.tsx b/lib/ts/recipe/multifactorauth/recipe.tsx index e6a71bb36..051385c43 100644 --- a/lib/ts/recipe/multifactorauth/recipe.tsx +++ b/lib/ts/recipe/multifactorauth/recipe.tsx @@ -18,7 +18,6 @@ */ import MultiFactorAuthWebJS from "supertokens-web-js/recipe/multifactorauth"; -import { MultiFactorAuthClaim } from "supertokens-web-js/recipe/multifactorauth"; import NormalisedURLPath from "supertokens-web-js/utils/normalisedURLPath"; import { PostSuperTokensInitCallbacks } from "supertokens-web-js/utils/postSuperTokensInitCallbacks"; import { SessionClaimValidatorStore } from "supertokens-web-js/utils/sessionClaimValidatorStore"; @@ -28,6 +27,7 @@ import RecipeModule from "../recipeModule"; import { DEFAULT_FACTOR_CHOOSER_PATH } from "./constants"; import { getFunctionOverrides } from "./functionOverrides"; +import { MultiFactorAuthClaimClass } from "./multiFactorAuthClaim"; import { normaliseMultiFactorAuthFeature } from "./utils"; import type { @@ -50,7 +50,10 @@ export default class MultiFactorAuth extends RecipeModule< static instance?: MultiFactorAuth; static RECIPE_ID = "multifactorauth"; - static MultiFactorAuthClaim = MultiFactorAuthClaim; + static MultiFactorAuthClaim = new MultiFactorAuthClaimClass( + () => MultiFactorAuth.getInstanceOrThrow().webJSRecipe, + (context) => this.getInstanceOrThrow().getDefaultRedirectionURL(context) + ); public recipeID = MultiFactorAuth.RECIPE_ID; public firstFactors: string[] = []; diff --git a/lib/ts/recipe/passwordless/recipe.tsx b/lib/ts/recipe/passwordless/recipe.tsx index 6a2fe6ac6..e0075cf5b 100644 --- a/lib/ts/recipe/passwordless/recipe.tsx +++ b/lib/ts/recipe/passwordless/recipe.tsx @@ -18,10 +18,12 @@ */ import PasswordlessWebJS from "supertokens-web-js/recipe/passwordless"; +import { PostSuperTokensInitCallbacks } from "supertokens-web-js/utils/postSuperTokensInitCallbacks"; import { SSR_ERROR } from "../../constants"; import { isTest } from "../../utils"; import AuthRecipe from "../authRecipe"; +import MultiFactorAuth from "../multifactorauth/recipe"; import { getFunctionOverrides } from "./functionOverrides"; import { normalisePasswordlessConfig } from "./utils"; @@ -67,6 +69,23 @@ export default class Passwordless extends AuthRecipe< ): RecipeInitResult { const normalisedConfig = normalisePasswordlessConfig(config); + PostSuperTokensInitCallbacks.addPostInitCallback(() => { + const mfa = MultiFactorAuth.getInstance(); + if (mfa !== undefined) { + mfa.addMFAFactors( + ["otp-phone", "otp-email", "link-phone", "link-email"], + [ + { + id: "otp-phone", + name: "Phone-based OTP", + description: "OTP delivered by a text message", + path: "/check-auth/otp", + }, + ] + ); + } + }); + return { recipeID: Passwordless.RECIPE_ID, authReact: ( diff --git a/test/server/index.js b/test/server/index.js index f001d7fdb..c112eca70 100644 --- a/test/server/index.js +++ b/test/server/index.js @@ -382,6 +382,18 @@ app.post( } ); +app.post("/payload", verifySession(), async (req, res) => { + let session = req.session; + + await session.mergeIntoAccessTokenPayload(req.body); + + res.send({ status: "OK" }); +}); + +app.get("/auth/mfa/info", (req, res) => { + res.send({ status: "OK" }); +}); + app.get("/token", async (_, res) => { res.send({ latestURLWithToken,