diff --git a/v3/docs/migration/account-migration.mdx b/v3/docs/migration/account-migration.mdx new file mode 100644 index 000000000..c3801346a --- /dev/null +++ b/v3/docs/migration/account-migration.mdx @@ -0,0 +1,734 @@ +--- +title: Account Migration +hide_title: true +sidebar_position: 2 +toc_max_heading_level: 4 +description: Migrate your users from a legacy authentication provider to SuperTokens. +--- + +import { Separator } from "/src/components/Separator"; +import { DescriptionText } from "/src/components/DescriptionText"; +import { HTTPRequestCard } from "/src/components/Cards"; +import { H2, H3 } from "/src/components/Typography"; +import { BackendTabs } from "/src/components/Tabs"; +import { Accordion } from "/src/components/Accordion"; + +import BulkImportUserRequest from "./_blocks/bulk-import-user-request.mdx" +import BulkImportUsersCountRequest from "./_blocks/count-staged-users-request.mdx" +import BulkImportUsersGetRequest from "./_blocks/get-staged-users-request.mdx" +import BulkImportUsersAddRequest from "./_blocks/add-users-for-bulk-import-request.mdx" + +# Account Migration + + + The following guide will show you how to move your users from your current authentication solution to **SuperTokens** + + + + +## Overview + +The process of migrating your accounts can be broken down into two parts: + +### Creating new users on the fly + +In order to ensure a smooth migration process, with no downtime, you need to be able to directly create new users from the legacy sign up flow. +This is necessary since there will be a time gap between when you export all your data for bulk import and when you go live with **SuperTokens**. + +New users might get created in that interval through your legacy authentication provider. +Hence, you will also need to create them in **SuperTokens** to keep the data in sync. + +### Adding most of your users through a bulk import + +After you have set in place the lazy migration process you can move on to adding most of your users. +This will happened through the bulk import API. +The process is asynchronous and can work with large amounts of data. + + + +## Before You Start + +This guide assumes that you have already integrated **SuperTokens** with your existing stack. +If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction) and explore all the supported [authentication methods](/docs/authentication/overview). + + +## Steps + +### 1. Update the legacy sign up flow + +Modify the legacy sign up flow logic to also create new users in **SuperTokens**. +This can be done through the `Import User` endpoint that allows you to directly create accounts. +Call the endpoint from the authentication flow used by your legacy provider. + +After you have added the new sign up logic you can deploy the changes and move to the next step. + +:::info +If your application does not have a sign up process or if new users are created manually you can skip this step +::: + + + + +```bash +curl --location --request POST '^{coreInfo.uri}/bulk-import/import' \ + --header 'api-key: ^{coreInfo.key}' \ + --header 'Content-Type: application/json; charset=utf-8' \ + --data ' + { + "externalUserId": "user_12345", + "userMetadata": { + "firstName": "Jane", + "lastName": "Doe", + "department": "Engineering" + }, + "userRoles": [{ "role": "admin", "tenantIds": [] }], + "totpDevices": [ + { + "secretKey": "JBSWY3DPEHPK3PXP", + "period": 30, + "skew": 1, + "deviceName": "Main Device" + } + ], + "loginMethods": [ + { + "isVerified": true, + "isPrimary": true, + "timeJoinedInMSSinceEpoch": 1672531199000, + "recipeId": "emailpassword", + "email": "jane.doe@example.com", + "passwordHash": "$2b$12$KIXQeFz...", + "hashingAlgorithm": "bcrypt" + } + ] + } +' +``` + + + + + ```tsx + const BASE_URL = '^{coreInfo.uri}'; + const API_KEY = '^{coreInfo.key}'; + + const url = `${BASE_URL}/bulk-import/import`; + const options = { + method: 'POST', + headers: { + 'api-key': API_KEY, + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify({ + externalUserId: "user_12345", + userRoles: [{ role: "admin", tenantIds: [] }], + userMetadata: { + firstName: "Jane", + lastName: "Doe", + department: "Engineering" + }, + totpDevices: [ + { + secretKey: "JBSWY3DPEHPK3PXP", + period: 30, + skew: 1, + deviceName: "Main Device" + } + ], + loginMethods: [ + { + isVerified: true, + isPrimary: true, + timeJoinedInMSSinceEpoch: 1672531199000, + recipeId: "emailpassword", + email: "jane.doe@example.com", + passwordHash: "$2b$12$KIXQeFz...", + hashingAlgorithm: "bcrypt" + } + ] + }) + } + + fetch(url, options) + .then(response => response.json()) + .then(json => console.log(json)) + .catch(err => console.error(err)); + ``` + + + + + ```go + + import ( + "fmt" + "net/http" + "strings" + "io" + ) + + func main() { + baseUrl := "^{coreInfo.uri}" + apiKey := "^{coreInfo.key}" + url := fmt.Sprintf("%s/bulk-import/import", baseUrl) + payload := `{ + "externalUserId": "user_12345", + "userMetadata": { + "firstName": "Jane", + "lastName": "Doe", + "department": "Engineering" + }, + "userRoles": [{ "role": "admin", "tenantIds": [] }], + "totpDevices": [ + { + "secretKey": "JBSWY3DPEHPK3PXP", + "period": 30, + "skew": 1, + "deviceName": "Main Device" + } + ], + "loginMethods": [ + { + "isVerified": true, + "isPrimary": true, + "timeJoinedInMSSinceEpoch": 1672531199000, + "recipeId": "emailpassword", + "email": "jane.doe@example.com", + "passwordHash": "$2b$12$KIXQeFz...", + "hashingAlgorithm": "bcrypt" + } + ] + }` + + req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) + + req.Header.Add("accept", "application/json") + req.Header.Add("api-key", apiKey) + req.Header.Add("content-type", "application/json") + + res, _ := http.DefaultClient.Do(req) + + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + fmt.Println(string(body)) + } + ``` + + + + + ```python + import requests + from typing import Dict, Any + + BASE_URL = "^{coreInfo.uri}" + API_KEY = "^{coreInfo.key}" + + url = f"{BASE_URL}/bulk-import/import" + + payload: Dict[str, Any] = { + "externalUserId": "user_12345", + "userMetadata": { + "firstName": "Jane", + "lastName": "Doe", + "department": "Engineering" + }, + "userRoles": [{ "role": "admin", "tenantIds": [] }], + "totpDevices": [ + { + "secretKey": "JBSWY3DPEHPK3PXP", + "period": 30, + "skew": 1, + "deviceName": "Main Device" + } + ], + "loginMethods": [ + { + "isVerified": True, + "isPrimary": True, + "timeJoinedInMSSinceEpoch": 1672531199000, + "recipeId": "emailpassword", + "email": "jane.doe@example.com", + "passwordHash": "$2b$12$KIXQeFz...", + "hashingAlgorithm": "bcrypt" + } + ] + } + + headers = { + "api-key": API_KEY, + "Content-Type": "application/json", + } + + response = requests.post(url, json=payload, headers=headers) + + print(response.json()) + ``` + + + + + + + +:::info +If your current authentication logic includes a password change flow, you will also have to update it, to keep the user data in sync. +::: + +:::info Email Verification During Import +When importing users, the `isVerified` field in each login method determines whether the email/phone will be marked as verified. Make sure to set this field correctly based on the verification status in your legacy system. +::: + +### 2. Export the accounts from your legacy provider + +Export the users from your legacy authentication provider and adjust the data to match the request body schema used in the [**`Add Users for Bulk Import`**](#add-users-for-bulk-import-http-request) endpoint. + +### 3. Perform the bulk migration process + +:::warning + +If your application has a sign up process please make sure that you have completed the [**first step**](#1-update-the-legacy-sign-up-flow). +Otherwise, new accounts that get created after you have exported your users will not be available in **SuperTokens**. + +::: + + +#### 3.1 Add the accounts that should be imported + +Using the data that you have generated in the previous step, call the `Add Users for Bulk Import` endpoint. +This step stages the data that will be imported later on by the background job. + +Keep in mind that the endpoint has a limit of **10000 users** per request. + + + + + +```bash +curl --location --request POST '^{coreInfo.uri}/bulk-import/users' \ + --header 'api-key: ^{coreInfo.key}' \ + --header 'Content-Type: application/json; charset=utf-8' \ + --data-raw '{ + users: [ + { + "externalUserId": "user_2", + "userMetadata": { + "firstName": "John", + "lastName": "Doe", + "department": "Marketing" + }, + "userRoles": [{ "role": "editor", "tenantIds": [] }], + "loginMethods": [ + { + "isVerified": true, + "isPrimary": true, + "timeJoinedInMSSinceEpoch": 1672617599000, + "recipeId": "thirdparty", + "email": "john.doe@gmail.com", + "thirdPartyId": "google", + "thirdPartyUserId": "google_987654321" + } + ] + } + ] +} +``` + + + + + ```tsx + const BASE_URL = '^{coreInfo.uri}'; + const API_KEY = '^{coreInfo.key}'; + + const url = `${BASE_URL}/bulk-import/users`; + const options = { + method: 'POST', + headers: { + 'api-key': API_KEY, + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify({ + users: [ + { + externalUserId: "user_2", + userMetadata: { + firstName: "John", + lastName: "Doe", + department: "Marketing" + }, + userRoles: [{ role: "editor", tenantIds: [] }], + loginMethods: [ + { + isVerified: true, + isPrimary: true, + timeJoinedInMSSinceEpoch: 1672617599000, + recipeId: "thirdparty", + email: "john.doe@gmail.com", + thirdPartyId: "google", + thirdPartyUserId: "google_987654321" + } + ] + } + ] + }) + } + + fetch(url, options) + .then(response => response.json()) + .then(json => console.log(json)) + .catch(err => console.error(err)); + ``` + + + + + ```go + import ( + "fmt" + "net/http" + "strings" + "io" + ) + + func main() { + baseUrl := "^{coreInfo.uri}" + apiKey := "^{coreInfo.key}" + url := fmt.Sprintf("%s/bulk-import/users", baseUrl) + payload := `{ + "users": [ + { + "externalUserId": "user_2", + "userMetadata": { + "firstName": "John", + "lastName": "Doe", + "department": "Marketing" + }, + "userRoles": [{ "role": "editor", "tenantIds": [] }], + "loginMethods": [ + { + "isVerified": true, + "isPrimary": true, + "timeJoinedInMSSinceEpoch": 1672617599000, + "recipeId": "thirdparty", + "email": "john.doe@gmail.com", + "thirdPartyId": "google", + "thirdPartyUserId": "google_987654321" + } + ] + } + ] + }` + + req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) + req.Header.Add("accept", "application/json") + req.Header.Add("api-key", apiKey) + req.Header.Add("content-type", "application/json") + + res, _ := http.DefaultClient.Do(req) + + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + fmt.Println(string(body)) + } + ``` + + + + + ```python + import requests + from typing import Dict, Any + + BASE_URL = "^{coreInfo.uri}" + API_KEY = "^{coreInfo.key}" + + url = f"{BASE_URL}/bulk-import/users" + + payload: Dict[str, Any] = { + "users": [ + { + "externalUserId": "user_2", + "userMetadata": { + "firstName": "John", + "lastName": "Doe", + "department": "Marketing" + }, + "userRoles": [{ "role": "editor", "tenantIds": [] }], + "loginMethods": [ + { + "isVerified": True, + "isPrimary": True, + "timeJoinedInMSSinceEpoch": 1672617599000, + "recipeId": "thirdparty", + "email": "john.doe@gmail.com", + "thirdPartyId": "google", + "thirdPartyUserId": "google_987654321" + } + ] + } + ] + } + + headers = { + "api-key": API_KEY, + "Content-Type": "application/json", + } + + response = requests.post(url, json=payload, headers=headers) + + print(response.json()) + ``` + + + + + + + +:::info The Bulk Import Cron Job + +Every 5 minutes the **SuperTokens** core service will run a cron job that goes through the staged users and tries to import them. +If a user gets imported successfully it will get removed from the staged list. + +::: + + +#### 3.2 Monitor the progress of the job + +In order to determine if all the users have been processed by the import flow call the [`Count Staged Users`](#count-staged-users-http-request) API. + +Before doing that, let's first understand the different states in which a staged user can be. +During the import process, the user can have one of the following statuses: +- **NEW**: The user has not yet been picked up by the import process. +- **PROCESSING**: The import process has selected the user for import. +- **FAILED**: The import process has failed for that user. + +If a user gets imported successfully it will then be removed from the staged list. Hence, no status is needed for that state. + +With this new information let's get back to the `count users` endpoint. +The request counts the users that are staged for import. +Pass a status filter as a query parameter (e.g. `status=NEW`) to count only the users with that status. + +Given that information, to check if your import is finalized do the following: +1. Call the `count users` API once without any filters. If the count is 0, then the import process is done. +2. If the count is not 0, then check if you still have rows that are getting processed (`status=PROCESSING`) or if there are rows that have not yet been picked up by the import job (`status=NEW`) +3. If the only rows that are left are the ones with the `FAILED` status, then proceed to step `3.3`. There you will see how to debug those issues. + + + + + +```bash + curl --location --request GET '^{coreInfo.uri}/bulk-import/users/count?status=PROCESSING' \ + --header 'api-key: ^{coreInfo.key}' \ + --header 'Content-Type: application/json; charset=utf-8' \ +``` + + + + + ```tsx + const BASE_URL = '^{coreInfo.uri}'; + const API_KEY = '^{coreInfo.key}'; + + const url = `${BASE_URL}/bulk-import/users/count?status=PROCESSING`; + const options = { + method: 'GET', + headers: { + 'api-key': API_KEY, + 'Content-Type': 'application/json; charset=utf-8', + }, + } + + fetch(url, options) + .then(response => response.json()) + .then(json => console.log(json)) + .catch(err => console.error(err)); + ``` + + + + + ```go + import ( + "fmt" + "net/http" + "io" + ) + + func main() { + baseUrl := "^{coreInfo.uri}" + apiKey := "^{coreInfo.key}" + url := fmt.Sprintf("%s/bulk-import/users/count?status=PROCESSING", baseUrl) + + req, _ := http.NewRequest("GET", url, nil) + + req.Header.Add("accept", "application/json") + req.Header.Add("api-key", apiKey) + req.Header.Add("content-type", "application/json") + + res, _ := http.DefaultClient.Do(req) + + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + fmt.Println(string(body)) + } + ``` + + + + + ```python + import requests + + BASE_URL = "^{coreInfo.uri}" + API_KEY = "^{coreInfo.key}" + + url = f"{BASE_URL}/bulk-import/users/count?status=PROCESSING" + + headers = { + "api-key": API_KEY, + "Content-Type": "application/json", + } + + response = requests.post(url, headers=headers) + + print(response.json()) + ``` + + + + + + + + + +#### 3.3 Handle staged users that failed to import + +Go through this step only if you have staged users that failed to import. +This can happen for a number of reasons. Some of the most common ones: +- `Email` / `phoneNumber` already exists +- `externalUserId` is being already used by other user +- A primary user already exists for the email but with a different login method + +If at the end of the previous step you have determined that you have staged users that failed to import debug the issues with the `get users` API. + + + + + ```bash + curl --location --request GET '^{coreInfo.uri}/bulk-import/users?status=FAILED' \ + --header 'api-key: ^{coreInfo.key}' \ + --header 'Content-Type: application/json; charset=utf-8' \ + ``` + + + + + ```tsx + const BASE_URL = '^{coreInfo.uri}'; + const API_KEY = '^{coreInfo.key}'; + + const url = `${BASE_URL}/bulk-import/users?status=FAILED`; + const options = { + method: 'GET', + headers: { + 'api-key': API_KEY, + 'Content-Type': 'application/json; charset=utf-8', + }, + } + + fetch(url, options) + .then(response => response.json()) + .then(json => console.log(json)) + .catch(err => console.error(err)); + ``` + + + + + ```go + import ( + "fmt" + "net/http" + "io" + ) + + func main() { + baseUrl := "^{coreInfo.uri}" + apiKey := "^{coreInfo.key}" + url := fmt.Sprintf("%s/bulk-import/users?status=FAILED", baseUrl) + + req, _ := http.NewRequest("GET", url, nil) + + req.Header.Add("accept", "application/json") + req.Header.Add("api-key", apiKey) + req.Header.Add("content-type", "application/json") + + res, _ := http.DefaultClient.Do(req) + + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + fmt.Println(string(body)) + } + ``` + + + + + ```python + import requests + + BASE_URL = "^{coreInfo.uri}" + API_KEY = "^{coreInfo.key}" + + url = f"{BASE_URL}/bulk-import/users?status=FAILED" + + headers = { + "api-key": API_KEY, + "Content-Type": "application/json", + } + + response = requests.post(url, headers=headers) + + print(response.json()) + ``` + + + + + + + + + +The response will include the import error messages for each specific user. +Use them to determine what needs to be corrected in your import data. +After you have fixed the issues, run the process again, from step `3.1`, only for the corrected data. + + + +:::success You have successfully migrated your accounts + + +If you all your data has been imported then you can now consider the account migration process as done. +Go on to the [session migration](/docs/migration/session-migration) step to complete the entire migration flow. + +::: + + +## See also + + + + + + + +