---
title: Migration from an older SuperTokens SDK to a newer one
description: Upgrade your SuperTokens SDK to enable MFA, ensuring compatibility and smooth transition for existing users.
sidebar:
  order: 3
  hidden: true
---

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.

<DependentContent passive group="backend-language">
<ContentOption title="Go" value="go">
:::note[At the moment this feature is not supported through the Go SDK.]
:::
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
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];
            },
          };
        },
      },
    }),
  ],
});
```
</Tab>
<Tab title="Go" value="go">

</Tab>
<Tab title="Python" value="python">
```python check=false reason="framework placeholder must be replaced for the target Python server"
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))
    ],
)
```
</Tab>
</CodeGroup>

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.
