Migration from an older SuperTokens SDK to a newer one
Upgrade your SuperTokens SDK to enable MFA, ensuring compatibility and smooth transition for existing users.
This section is applicable to those who want to enable MFA for the first time and are already using SuperTokens in production. The following are the steps you need to take:
- Make sure that you have updated your SuperTokens core, backend SDK and frontend SDK that supports MFA (you can find these versions in the CHANGELOG files in their respective GitHub repository). Make sure to read the migration guide for each of the breaking version upgrades. You should aim to get your existing feature set working with the new version of SuperTokens before you enable MFA.
- Follow the backend and frontend setup we have in this guide along with the factor specific setup (TOTP or email / SMS OTP).
If enabling MFA for all users at once
If you have enabled MFA for all users, then existing logged in users will be asked to complete the secondary factor as soon as they visit your app / website once you have pushed the changes to production. This happens because their existing session is modified to add the MFA claim into it, however, the v value in the claim will be false since they have not completed MFA yet. This would fail the validators on the frontend which would redirect the user to the MFA login screen.
If you have clients like mobile apps, which take time to upgrade across your entire user base, you can temporarily exempt a server-managed migration cohort from the global MFA validator:
The application helper used below must look up eligibility in application-owned server-side storage. Bind each record to the authenticated user and session or to a registered device. Return false if the identity or binding is missing, invalid, expired, or outside the cohort. Also return false for sensitive operations, such as account recovery, credential changes, payments, or administrative actions, so that they always require MFA step-up.
You may use the exact legacy client-version value (1.0 in this example) only as one input when enrolling the user, session, or device into the server-side cohort. Enrollment must also satisfy server-verified migration policy; the client-controlled header must not create an eligibility record by itself or authorize an exemption at request time. Missing, malformed, and unknown values must not create an enrollment. Set an expiry on every record and a migration sunset date when you will require version 2.0 and remove the exception.
import Session from "supertokens-node/recipe/session";
import { ClaimValidationResult } from "supertokens-node/recipe/session/types";
import SuperTokens, { RecipeUserId } from "supertokens-node";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import { UserContext } from "supertokens-node/types";
async function isMfaMigrationExemptionEligible(input: {
tenantId: string;
userId: string;
recipeUserId: RecipeUserId;
userContext: UserContext;
}): Promise<boolean> {
// Replace this with the server-side lookup described above. Throwing here
// ensures that an unimplemented lookup cannot disable MFA.
throw new Error("MFA migration cohort lookup is not implemented");
}
SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
recipeList: [
// other recipes..
Session.init({
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getGlobalClaimValidators: (input) => {
// We remove the existing default MFA validator which checks that
// the client has finished MFA.
let newValidatorsArray = input.claimValidatorsAddedByOtherRecipes.filter(
(v) => v.id !== MultiFactorAuth.MultiFactorAuthClaim.key,
);
// We create an instance of the default validator that is added
// by SuperTokens so that we can make modifications to it
let originalValidator =
MultiFactorAuth.MultiFactorAuthClaim.validators.hasCompletedMFARequirementsForAuth();
// We create a custom validator based on the default validator
let customValidator = {
...originalValidator,
validate: async (payload: any, userContext: UserContext): Promise<ClaimValidationResult> => {
if (
await isMfaMigrationExemptionEligible({
tenantId: input.tenantId,
userId: input.userId,
recipeUserId: input.recipeUserId,
userContext,
})
) {
return {
isValid: true,
};
}
// for newer clients, we call the original validate function
// which will check the claim value in the session.
return originalValidator.validate(payload, userContext);
},
};
return [customValidator, ...newValidatorsArray];
},
};
},
},
}),
],
});from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import session
from supertokens_python.recipe.session.utils import InputOverrideConfig
from supertokens_python.recipe.session.interfaces import RecipeInterface
from supertokens_python.recipe.session.interfaces import (
SessionClaimValidator,
ClaimValidationResult,
)
from typing import Dict, Any, List
from supertokens_python.types import RecipeUserId
from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import (
MultiFactorAuthClaim,
)
async def is_mfa_migration_exemption_eligible(
tenant_id: str,
user_id: str,
recipe_user_id: RecipeUserId,
user_context: Dict[str, Any],
) -> bool:
# Replace this with the server-side lookup described above. Raising here
# ensures that an unimplemented lookup cannot disable MFA.
raise NotImplementedError("MFA migration cohort lookup is not implemented")
def override_functions(
original_implementation: RecipeInterface,
) -> RecipeInterface:
def get_global_claim_validators(
tenant_id: str,
user_id: str,
recipe_user_id: RecipeUserId,
claim_validators_added_by_other_recipes: List[SessionClaimValidator],
user_context: Dict[str, Any],
):
# Remove the existing default MFA validator
new_validators = [
v
for v in claim_validators_added_by_other_recipes
if v.id != MultiFactorAuthClaim.key
]
# Create an instance of the default validator
original_validator = (
MultiFactorAuthClaim.validators.has_completed_mfa_requirements_for_auth()
)
original_validator_validate_func = original_validator.validate
# Create a custom validator based on the default validator
async def custom_validate(
payload: Any, user_context: Dict[str, Any]
) -> ClaimValidationResult:
if await is_mfa_migration_exemption_eligible(
tenant_id,
user_id,
recipe_user_id,
user_context,
):
return ClaimValidationResult(is_valid=True)
# For newer clients, call the original validate function
return await original_validator_validate_func(payload, user_context)
original_validator.validate = custom_validate
return [original_validator, *new_validators]
original_implementation.get_global_claim_validators = get_global_claim_validators
return original_implementation
init(
app_info=InputAppInfo(
app_name="...",
api_domain="...",
website_domain="...",
),
supertokens_config=SupertokensConfig(
connection_uri="...",
),
framework="...",
recipe_list=[
session.init(override=InputOverrideConfig(functions=override_functions))
],
)The code above still adds the MFA claim to all sessions with its v boolean set to false. It skips backend validation only when the application confirms that the authenticated user and session or registered device belongs to the temporary migration cohort. Apply the original validator to every sensitive operation, regardless of cohort membership.