File length: 2201302 # Quickstart - Frontend Setup Source: https://supertokens.com/docs/quickstart/frontend-setup Start the setup by configuring your frontend application to use **SuperTokens** for authentication. This guide uses the **SuperTokens pre-built UI** components. If you want to create your own interface please check the **Custom UI** tutorial. ## 1. Install the SDK Run the following command in your terminal to install the package. ```bash npm i -s supertokens-auth-react ``` ```bash npm i -s supertokens-auth-react supertokens-web-js ``` ```bash yarn add supertokens-auth-react supertokens-web-js ``` ```bash pnpm add supertokens-auth-react supertokens-web-js ``` ```bash npm i -s supertokens-web-js ``` ```bash npm i -s supertokens-web-js ``` ```bash yarn add supertokens-web-js ``` ```bash pnpm add supertokens-web-js ``` ```bash npm i -s supertokens-web-js ``` ```bash npm i -s supertokens-web-js ``` ```bash yarn add supertokens-web-js ``` ```bash pnpm add supertokens-web-js ``` ## 2. Initialize the SDK In your main application file call the `SuperTokens.init` function to initialize the SDK. The `init` call includes the [main configuration details](/docs/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. After that you have to wrap the application with the `SuperTokensWrapper` component. This provides authentication context for the rest of the UI tree. ```tsx // highlight-start SuperTokens.init({ appInfo: { // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [EmailPassword.init(), Session.init()], }); // highlight-end /* Your App */ class App extends React.Component { render() { return ( // highlight-next-line {/*Your app components*/} // highlight-next-line ); } } ``` Before we initialize the `supertokens-web-js` SDK let's see how we use it in our Angular app. **Architecture** - The `supertokens-web-js` SDK is responsible for session management and providing helper functions to check if a session exists, or validate the access token claims on the frontend (for example, to check for user roles before showing some UI). We initialise this SDK on the root of your Angular app, so that all pages in your app can use it. - You have to create a `*` route in the Angular app which renders our pre-built UI. which also needs to be initialised, but only on that route. **Creating the `` route** - Use the Angular CLI to generate a new route ```bash ng generate module auth --route auth --module app.module ``` - Add the following code to your `auth` angular component ```tsx title="/app/auth/auth.component.ts" @Component({ selector: "app-auth", template: '
', }) export class AuthComponent implements OnDestroy, AfterViewInit { constructor( private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, ) {} ngAfterViewInit() { this.loadScript("^{prebuiltUIVersion}"); } ngOnDestroy() { // Remove the script when the component is destroyed const script = this.document.getElementById("supertokens-script"); if (script) { script.remove(); } } private loadScript(src: string) { const script = this.renderer.createElement("script"); script.type = "text/javascript"; script.src = src; script.id = "supertokens-script"; script.onload = () => { supertokensUIInit({ appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ supertokensUIEmailPassword.init(), supertokensUISession.init(), ], }); }; this.renderer.appendChild(this.document.body, script); } } ``` - In the `loadScript` function, we provide the SuperTokens config for the UI. We add the emailpassword and session recipe. - Initialize the `supertokens-web-js` SDK in your angular app's root component. This provides session management across your entire application. ```tsx title="/app/auth/auth.component.ts" @Component({ selector: "app-auth", template: '
', }) export class AuthComponent implements OnDestroy, AfterViewInit { constructor( private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, ) {} ngAfterViewInit() { this.loadScript("^{prebuiltUIVersion}"); } ngOnDestroy() { // Remove the script when the component is destroyed const script = this.document.getElementById("supertokens-script"); if (script) { script.remove(); } } private loadScript(src: string) { const script = this.renderer.createElement("script"); script.type = "text/javascript"; script.src = src; script.id = "supertokens-script"; script.onload = () => { supertokensUIInit({ appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ supertokensUIEmailPassword.init(), supertokensUISession.init(), ], }); }; this.renderer.appendChild(this.document.body, script); } } ```
Before we initialize the `supertokens-web-js` SDK let's see how we use it in our Vue app **Architecture** - The `supertokens-web-js` SDK is responsible for session management and providing helper functions to check if a session exists, or validate the access token claims on the frontend (for example, to check for user roles before showing some UI). We initialise this SDK on the root of your Vue app, so that all pages in your app can use it. - We create a `*` route in the Vue app which renders our pre-built UI which also needs to be initialised, but only on that route. **Creating the `` route** - Create a new file `AuthView.vue`, this Vue component is used to render the auth component: ```tsx ``` - In the `loadScript` function, we provide the SuperTokens config for the UI. We add the emailpassword and session recipe. - Initialize the `supertokens-web-js` SDK in your Vue app's `main.ts` file. This provides session management across your entire application. ```tsx title="/main.ts " // @ts-ignore // @ts-ignore // @ts-ignore SuperTokens.init({ appInfo: { appName: "", apiDomain: "", apiBasePath: "", }, recipeList: [Session.init()], }); const app = createApp(App); app.use(router); app.mount("#app"); ```
## 3. Configure Routing In order for the **pre-built UI** to be rendered inside your application, you have to specify which routes show the authentication components. The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. Call the `getSuperTokensRoutesForReactRouterDom` method from within any `react-router-dom` `Routes` component. ```tsx // highlight-next-line class App extends React.Component { render() { return ( {/*This renders the login UI on the route*/} // highlight-next-line {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [EmailPasswordPreBuiltUI])} {/*Your app routes*/} ); } } ``` :::important If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample. Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details. ```tsx function AppRoutes() { const authRoutes = getSuperTokensRoutesForReactRouterDom( reactRouterDom, [/* Add your UI recipes here e.g. EmailPasswordPrebuiltUI, PasswordlessPrebuiltUI, ThirdPartyPrebuiltUI */] ); const routes = useRoutes([ ...authRoutes.map(route => route.props), // Include the rest of your app routes ]); return routes; } function App() { return ( ); } ``` ::: Add the highlighted code snippet to your root level `render` function. ```tsx class App extends React.Component { render() { // highlight-start if (canHandleRoute([EmailPasswordPreBuiltUI])) { // This renders the login UI on the route return getRoutingComponent([EmailPasswordPreBuiltUI]) } // highlight-end return ( {/*Your app*/} ); } } ``` Update your angular router so that all auth related requests load the `auth` component ```tsx title="/app/app-routing.module.ts" const routes: Routes = [ // highlight-start { path: "^{appInfo.websiteBasePath_withoutForwardSlash}", // @ts-ignore loadChildren: () => import("./auth/auth.module").then((m) => m.AuthModule), }, { path: "**", // @ts-ignore loadChildren: () => import("./home/home.module").then((m) => m.HomeModule), }, // highlight-end ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ``` Update your Vue router so that all auth related requests load the `AuthView` component ```tsx title="/router/index.ts" // @ts-ignore // @ts-ignore // @ts-ignore const router = createRouter({ // @ts-ignore history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: "/", name: "home", component: HomeView, }, { path: "/:pathMatch(.*)*", name: "auth", component: AuthView, }, ], }); export default router; ``` ## 4. Handle Session Tokens This part is handled automatically by the **Frontend SDK**. You don't need to do anything. The step serves more as a way for us to tell you how is this handled under the hood. After you call the `init` function, the **SDK** adds interceptors to both `fetch` and `XHR`, XMLHTTPRequest. The latter is used by the `axios` library. The interceptors save the session tokens that are generated from the authentication flow. Those tokens are then added to requests initialized by your frontend app which target the backend API. By default, the tokens are stored through session cookies but you can also switch to [header based authentication](/docs/post-authentication/session-management/switch-between-cookies-and-header-authentication). ## 5. Secure Application Routes In order to prevent unauthorized access to certain parts of your frontend application you can use our utilities. Follow the code samples below to understand how to do this. You can wrap your components with the `` react component. This ensures that your component renders only if the user is logged in. If they are not logged in, the user is redirected to the login page. ```tsx // highlight-next-line // @ts-ignore class App extends React.Component { render() { return ( {/*Components that require to be protected by authentication*/} // highlight-end } /> ); } } ``` You can use the `doesSessionExist` function to check if a session exists in all your routes. ```tsx async function doesSessionExist() { if (await Session.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` You can use the `doesSessionExist` function to check if a session exists in all your routes. ```tsx async function doesSessionExist() { if (await Session.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ## 6. View the login UI You can check the login UI by visiting the `` route, in your frontend application. To review all the components of our pre-built UI please follow [this link](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/auth-page--playground).
## 1. Install the SDK Use the following command to install the required package. ```bash npm i -s supertokens-web-js ``` You need to add all of the following scripts to your app ```html ``` :::info If you want to implement a common authentication experience for both web and mobile, please look at our [**Unified Login guide**](/docs/authentication/unified-login/introduction). ::: ```bash npm i -s supertokens-react-native # IMPORTANT: If you already have @react-native-async-storage/async-storage as a dependency, make sure the version is 1.12.1 or higher npm i -s @react-native-async-storage/async-storage ``` Add to your `settings.gradle`: ```bash dependencyResolutionManagement { ... repositories { ... maven { url 'https://jitpack.io' } } } ``` Add the following to you app level's `build.gradle`: ```bash implementation 'com.github.supertokens:supertokens-android:X.Y.Z' ``` You can find the latest version of the SDK [here](https://github.com/supertokens/supertokens-android/releases) (ignore the `v` prefix in the releases). #### Using Cocoapods Add the Cocoapod dependency to your Podfile ```bash pod 'SuperTokensIOS' ``` #### Using Swift Package Manager Follow the [official documentation](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) to learn how to use Swift Package Manager to add dependencies to your project. When adding the dependency use the `master` branch after you enter the supertokens-ios repository URL: ```bash https://github.com/supertokens/supertokens-ios ``` Add the dependency to your pubspec.yaml ```bash supertokens_flutter: ^X.Y.Z ``` You can find the latest version of the SDK [here](https://github.com/supertokens/supertokens-flutter/releases) (ignore the `v` prefix in the releases). ## 2. Initialise SuperTokens Call the SDK init function at the start of your application. The invocation includes the [main configuration details](/docs/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. ```tsx SuperTokens.init({ appInfo: { apiDomain: "", apiBasePath: "", appName: "...", }, recipeList: [ Session.init(), EmailPassword.init(), ], }); ``` ```tsx supertokens.init({ appInfo: { apiDomain: "", apiBasePath: "", appName: "...", }, recipeList: [ supertokensSession.init(), supertokensEmailPassword.init(), ], }); ``` ```tsx SuperTokens.init({ apiDomain: "", apiBasePath: "", }); ``` Add the `SuperTokens.init` function call at the start of your application. ```kotlin ```swift ```dart void main() { SuperTokens.init( apiDomain: "", apiBasePath: "", ); } ``` ## 3. Add the Login UI The **Email/Password** flow involves two types of user interfaces. One for registering and creating new users, the *Sign Up Form*. And one for the actual authentication attempt, the *Sign In Form*. If you are provisioning users from a different method you can skip over adding the sign up form. ### 3.1 Add the Sign Up form For the **Sign Up** flow you have to first add the UI elements which render your form. After that, call the following function when the user submits the form that you have previously created. ```tsx async function signUpClicked(email: string, password: string) { try { let response = await signUp({ formFields: [{ id: "email", value: email }, { id: "password", value: password }] }) if (response.status === "FIELD_ERROR") { // one of the input formFields failed validation response.formFields.forEach(formField => { if (formField.id === "email") { // Email validation failed (for example incorrect email syntax), // or the email is not unique. window.alert(formField.error) } else if (formField.id === "password") { // Password validation failed. // Maybe it didn't match the password strength window.alert(formField.error) } }) } else if (response.status === "SIGN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign up was not allowed. window.alert(response.reason) } else { // sign up successful. The session tokens are automatically handled by // the frontend SDK. window.location.href = "/homepage" } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function signUpClicked(email: string, password: string) { try { let response = await supertokensEmailPassword.signUp({ formFields: [{ id: "email", value: email }, { id: "password", value: password }] }) if (response.status === "FIELD_ERROR") { // one of the input formFields failed validation response.formFields.forEach(formField => { if (formField.id === "email") { // Email validation failed (for example incorrect email syntax), // or the email is not unique. window.alert(formField.error) } else if (formField.id === "password") { // Password validation failed. // Maybe it didn't match the password strength window.alert(formField.error) } }) } else if (response.status === "SIGN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in was not allowed. window.alert(response.reason) } else { // sign up successful. The session tokens are automatically handled by // the frontend SDK. window.location.href = "/homepage" } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` For the **Sign Up** flow you have to first add the UI elements which render your form. After that, call the following API when the user submits the form that you have previously created. ```bash curl --location --request POST '/signup' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "formFields": [{ "id": "email", "value": "john@example.com" }, { "id": "password", "value": "somePassword123" }] }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: User creation was successful. The response also contains more information about the user, for example their user ID. - `status: "FIELD_ERROR"`: One of the form field inputs failed validation. The response body contains information about which form field input based on the `id`: - The email could fail validation if it's syntactically not an email, of it it's not unique. - The password could fail validation if it's not string enough (as defined by the backend password validator). Either way, you want to show the user an error next to the input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. - `status: "SIGN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign up was not allowed. The `formFields` input is a key-value array. You must provide it an `email` and a `password` value at a minimum. If you want to provide additional items, for example the user's name or age, you can append it to the array like so: ```json { "formFields": [{ "id": "email", "value": "john@example.com" }, { "id": "password", "value": "somePassword123" }, { "id": "name", "value": "John Doe" }] } ``` On the backend, the `formFields` array is available to you for consumption. On success, the backend sends back session tokens as part of the response headers which are automatically handled by our frontend SDK for you. #### How to check if an email is unique As a part of the sign up form, you may want to explicitly check that the entered email is unique. Whilst this is already done via the sign up API call, it may be a better UX to warn the user about a non unique email right after they finish typing it. ```tsx async function checkEmail(email: string) { try { let response = await doesEmailExist({ email }); if (response.doesExist) { window.alert("Email already exists. Please sign in instead") } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function checkEmail(email: string) { try { let response = await supertokensEmailPassword.doesEmailExist({ email }); if (response.doesExist) { window.alert("Email already exists. Please sign in instead") } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash curl --location --request GET '/emailpassword/email/exists?email=john@example.com' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: The response also contains a `exists` boolean which is `true` if the input email already belongs to an email password user. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. ### 3.2 Add the Sign In Form For the **Sign In** flow you have to first add the UI elements which render your form. After that, call the following function when the user submits the form that you have previously created. ```tsx async function signInClicked(email: string, password: string) { try { let response = await signIn({ formFields: [{ id: "email", value: email }, { id: "password", value: password }] }) if (response.status === "FIELD_ERROR") { response.formFields.forEach(formField => { if (formField.id === "email") { // Email validation failed (for example incorrect email syntax). window.alert(formField.error) } }) } else if (response.status === "WRONG_CREDENTIALS_ERROR") { window.alert("Email password combination is incorrect.") } else if (response.status === "SIGN_IN_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in was not allowed. window.alert(response.reason) } else { // sign in successful. The session tokens are automatically handled by // the frontend SDK. window.location.href = "/homepage" } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function signInClicked(email: string, password: string) { try { let response = await supertokensEmailPassword.signIn({ formFields: [{ id: "email", value: email }, { id: "password", value: password }] }) if (response.status === "FIELD_ERROR") { // one of the input formFields failed validation response.formFields.forEach(formField => { if (formField.id === "email") { // Email validation failed (for example incorrect email syntax). window.alert(formField.error) } }) } else if (response.status === "WRONG_CREDENTIALS_ERROR") { window.alert("Email password combination is incorrect.") } else if (response.status === "SIGN_IN_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in was not allowed. window.alert(response.reason) } else { // sign in successful. The session tokens are automatically handled by // the frontend SDK. window.location.href = "/homepage" } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` For the **Sign In** flow you have to first add the UI elements which render your form. After that, call the following API when the user submits the form that you have previously created. ```bash curl --location --request POST '/signin' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "formFields": [{ "id": "email", "value": "john@example.com" }, { "id": "password", "value": "somePassword123" }] }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in was successful. The response also contains more information about the user, for example their user ID. - `status: "WRONG_CREDENTIALS_ERROR"`: The input email and password combination is incorrect. - `status: "FIELD_ERROR"`: This indicates that the input email did not pass the backend validation - probably because it's syntactically not an email. You want to show the user an error next to the email input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. - `status: "SIGN_IN_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in was not allowed. On success, the backend sends back session tokens as part of the response headers which are automatically handled by our frontend SDK for you. ## 4. Handle Session Tokens You can use sessions with SuperTokens in two modes: - Using `httpOnly` cookies - Authorization bearer token. Our frontend SDK uses `httpOnly` cookie based session for websites by default as it secures against tokens theft via XSS attacks. For other platforms, like mobile apps, we use a bearer token in the `Authorization` header by default. ### With the Frontend SDK :::success No action required. ::: Our frontend SDK handles everything for you. You only need to make sure that you have called `supertokens.init` before making any network requests. Our SDK adds interceptors to `fetch` and `XHR` (used by `axios`) to save and add session tokens from and to the request. By default, our web SDKs use cookies to provide credentials. Our frontend SDK handles everything for you. You only need to make sure that you have added our network interceptors as shown below :::note By default our mobile SDKs use a bearer token in the Authorization header to provide credentials. ::: { return ( Are you using axios.create? ) }}> ```tsx // highlight-next-line let axiosInstance = axios.create({/*...*/}); // highlight-next-line SuperTokens.addAxiosInterceptors(axiosInstance); async function callAPI() { // use axios as you normally do let response = await axiosInstance.get("https://yourapi.com"); } ``` :::important You must call `addAxiosInterceptors` on all `axios` imports. ::: ```tsx // highlight-start SuperTokens.addAxiosInterceptors(axios); // highlight-end async function callAPI() { // use axios as you normally do let response = await axios.get("https://yourapi.com"); } ``` :::success When using `fetch`, network interceptors are added automatically when you call `supertokens.init`. So no action needed here. ::: ```kotlin ```dart // Import http from the SuperTokens package Future makeRequest() async { Uri uri = Uri.parse("http://localhost:3001/api"); var response = await http.get(uri); // handle response } ```

Using a custom HTTP client

If you use a custom HTTP client and want to use SuperTokens, you can simply provide the SDK with your client. All requests continue to use your client along with the session logic that SuperTokens provides. ```dart // Import http from the SuperTokens package Future makeRequest() async { Uri uri = Uri.parse("http://localhost:3001/api"); // Initialise your custom client var customClient = http.Client(); // provide your custom client to SuperTokens var httpClient = http.Client(client: customClient); var response = await httpClient.get(uri); // handle response } ```

Add the SuperTokens interceptor

Use the extension method provided by the SuperTokens SDK to enable interception on your `Dio` client. This allows the SuperTokens SDK to handle session tokens for you. ```dart void setup() { Dio dio = Dio(); // Create a Dio instance. dio.addSupertokensInterceptor(); } ```

Making network requests

You can make requests as you normally would with `dio`. ```dart void setup() { Dio dio = Dio( // Provide your config here ); dio.addSupertokensInterceptor(); var response = dio.get("http://localhost:3001/api"); // handle response } ```
:::note By default our mobile SDKs use a bearer token in the Authorization header to provide credentials. :::
### Without the Frontend SDK :::caution We highly recommend using our frontend SDK to handle session token management. It saves you a lot of time. ::: In this case, you need to manually handle the tokens and session refreshing, and decide if you are going to use header or cookie-based sessions. For browsers, we recommend cookies, while for mobile apps (or if you don't want to use the built-in cookie manager) you should use header-based sessions. #### During the Login Action You should attach the `st-auth-mode` header to calls to the login API, but this header is safe to attach to all requests. In this case it should be set to "cookie". The login API returns the following headers: - `Set-Cookie`: This contains the `sAccessToken`, `sRefreshToken` cookies which are `httpOnly` and are automatically managed by the browser. For mobile apps, you need to setup cookie handling yourself, use our SDK or use a header based authentication mode. - `front-token` header: This contains information about the access token: - The userID - The expiry time of the access token - The payload added by you in the access token. Here is the structure of the token: ```tsx let frontTokenFromRequestHeader = "..."; let frontTokenDecoded = JSON.parse(decodeURIComponent(escape(atob(frontTokenFromRequestHeader)))); console.log(frontTokenDecoded); /* { ate: 1665226412455, // time in milliseconds for when the access token expires, and then a refresh is required uid: "....", // user ID up: { sub: "..", iat: .., ... // other access token payload } } */ ``` This token is mainly used for cookie based auth because you don't have access to the actual access token on the frontend (for security reasons), but may want to read its payload (for example to know the user's role). This token itself is not signed and hence can't be used in place of the access token itself. You may want to save this token in `localstorage` or in frontend cookies (using `document.cookies`). - `anti-csrf` header (optional): By default it's not required, so it's not sent. But if this is sent, you should save this token as well for use when making requests. #### When You Make Network Requests to Protected APIs The `sAccessToken` gets attached to the request automatically by the browser. Other than that, you need to add the following headers to the request: - `rid: "anti-csrf"` - this prevents against anti-CSRF requests. If your `apiDomain` and `websiteDomain` values are exactly the same, then this is not necessary. - `anti-csrf` header (optional): If this was provided to you during login, then you need to add that token as the value of this header. - You need to set the `credentials` header to `true` or `include`. This is achieved different based on the library you are using to make requests. An API call can potentially update the `sAccessToken` and `front-token` tokens, for example if you call the `mergeIntoAccessTokenPayload` function on the `session` object on the backend. This kind of update is reflected in the response headers for your API calls. The headers contain new values for: - `sAccessToken`: This is as a new `Set-Cookie` header and is managed by the browser automatically. - `front-token`: This should be read and saved by you in the same way as it's being done during login. #### Handling session refreshing If any of your API calls return with a status code of `401`, it means that the access token has expired. This requires you to refresh the session before retrying the same API call. You can call the refresh API as follows: ```bash curl --location --request POST '/session/refresh' \ --header 'Cookie: sRefreshToken=...' ``` :::note - You may also need to add the `anti-csrf` header to the request if that was provided to you during sign in. - The cURL command above shows the `sRefreshToken` cookie as well, but this is added by the web browser automatically, so you don't need to add it explicitly. ::: The result of a session refresh is either: - Status code `200`: This implies a successful refresh. The set of tokens returned here is the same as when the user logs in, so you can handle them in the same way. - Status code `401`: This means that the refresh token is invalid, or has been revoked. You must ask the user to login again. Remember to clear the `front-token` that you saved on the frontend earlier. ##### During the Login Action You should attach the `st-auth-mode` header to calls to the login API, but this header is safe to attach to all requests. In this case it should be set to "header". The login API returns the following headers: - `st-access-token`: This contains the current access token associated with the session. You should save this in your application (e.g., in frontend `localstorage`). - `st-refresh-token`: This contains the current refresh token associated with the session. You should save this in your application (e.g., in frontend `localstorage`). #### When You Make Network Requests to Protected APIs You need to add the following headers to request: - `authorization: Bearer {access-token}` - You need to set the `credentials` to `true` or `include`. This is achieved different based on the library you are using to make requests. An API call can potentially update the `access-token`, for example if you call the `mergeIntoAccessTokenPayload` function on the `session` object on the backend. This kind of update is reflected in the response headers for your API calls. The headers contain new values for `st-access-token` These should be read and saved by you in the same way as it's being done during login. #### Handling session refreshing If any of your API calls return with a status code of `401`, it means that the access token has expired. This requires you to refresh the session before retrying the same API call. You can call the refresh API as follows: ```bash curl --location --request POST '/session/refresh' \ --header 'authorization: Bearer {refresh-token}' ``` The result of a session refresh is either: - Status code `200`: This implies a successful refresh. The set of tokens returned here is the same as when the user logs in, so you can handle them in the same way. - Status code `401`: This means that the refresh token is invalid, or has been revoked. You must ask the user to login again. Remember to clear the `st-refresh-token` and `st-access-token` that you saved on the frontend earlier. ## 5. Protect Frontend Routes You can use the `doesSessionExist` function to check if a session exists. ```tsx async function doesSessionExist() { if (await Session.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` You can use the `doesSessionExist` function to check if a session exists. ```tsx async function doesSessionExist() { if (await Session.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` You can use the `doesSessionExist` function to check if a session exists. ```tsx async function doesSessionExist() { if (await SuperTokens.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` You can use the `doesSessionExist` function to check if a session exists. ```kotlin Future doesSessionExist() async { return await SuperTokens.doesSessionExist(); } ``` ## 6. Add a Sign Out Action The `signOut` method revokes the session on the frontend and on the backend. Calling this function without a valid session also yields a successful response. ```tsx async function logout () { // highlight-next-line await Session.signOut(); window.location.href = "/auth"; // or to wherever your logic page is } ``` ```tsx async function logout () { // highlight-next-line await supertokensSession.signOut(); window.location.href = "/auth"; // or to wherever your logic page is } ``` ```tsx async function logout () { // highlight-next-line await SuperTokens.signOut(); // navigate to the login screen.. } ``` ```kotlin // navigate to the login screen.. } } ``` ```swift Future signOut() async { await SuperTokens.signOut( completionHandler: (error) { // handle error if any } ); } ``` - On success, the `signOut` function does not redirect the user to another page, so you must redirect the user yourself. - The `signOut` function calls the sign out API exposed by the session recipe on the backend. - If you call the `signOut` function whilst the access token has expired, but the refresh token still exists, our SDKs do an automatic session refresh before revoking the session.
:::success 🎉 Congratulations 🎉 Congratulations! You've successfully integrated your frontend app with SuperTokens. The [next section](./backend-setup) guides you through setting up your backend and then you should be able to complete a login flow. ::: # Quickstart - Backend Setup Source: https://supertokens.com/docs/quickstart/backend-setup Let's got through the changes required so that your backend can expose the **SuperTokens** authentication features. ## 1. Install the SDK Run the following command in your terminal to install the package. ```bash npm i -s supertokens-node ``` ```bash go get github.com/supertokens/supertokens-golang ``` ```bash pip install supertokens-python ``` :::info At the moment we only have SDKs for **Node.js**, **Python** and **Go**. If you wish to use **SuperTokens** with other languages you will have to create a separate authentication service. Please check [our guide](/docs/references/backend-sdks/other-frameworks) that shows you how to resolve this. ::: ## 2. Initialize the SDK You will have to initialize the **Backend SDK** alongside the code that starts your server. The init call will include [configuration details](/docs/references/backend-sdks/reference#sdk-configuration) for your app, how the backend will connect to the **SuperTokens Core**, as well as the **Recipes** that will be used in your setup. ```tsx supertokens.init({ framework: "express", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init() // initializes session features ] }); ``` ```tsx supertokens.init({ framework: "hapi", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init() // initializes session features ] }); ``` ```tsx supertokens.init({ framework: "fastify", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init() // initializes session features ] }); ``` ```tsx supertokens.init({ framework: "koa", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init() // initializes session features ] }); ``` ```tsx supertokens.init({ framework: "loopback", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init() // initializes session features ] }); ``` ```go ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import emailpassword, session init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="", website_base_path="" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io^{derived.appIdPathname}", # api_key: ), framework='fastapi', recipe_list=[ session.init(), # initializes session features emailpassword.init() ], mode='asgi' # use wsgi if you are running using gunicorn ) ``` ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import emailpassword, session init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="", website_base_path="" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io^{derived.appIdPathname}", # api_key: ), framework='flask', recipe_list=[ session.init(), # initializes session features emailpassword.init() ] ) ``` ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import emailpassword, session init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="", website_base_path="" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io^{derived.appIdPathname}", # api_key: ), framework='django', recipe_list=[ session.init(), # initializes session features emailpassword.init() ], mode='asgi' # use wsgi if you are running django server in sync mode ) ``` ## 3. Add the SuperTokens APIs and Configure CORS Now that the SDK is initialized you need to expose the endpoints that will be used by the frontend SDKs. Besides this, your server's CORS, Cross-Origin Resource Sharing, settings should be updated to allow the use of the authentication headers required by **SuperTokens**. :::important - Add the `middleware` BEFORE all your routes. - Add the `cors` middleware BEFORE the SuperTokens middleware as shown below. ::: ```tsx let app = express(); app.use( cors({ // highlight-start origin: "", allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], credentials: true, // highlight-end }), ); // IMPORTANT: CORS should be before the below line. // highlight-next-line app.use(middleware()); // ...your API routes ``` Register the `plugin`. ```tsx let server = Hapi.server({ port: 8000, routes: { // highlight-start cors: { origin: [""], additionalHeaders: [...supertokens.getAllCORSHeaders()], credentials: true, }, // highlight-end }, }); (async () => { // highlight-next-line await server.register(plugin); await server.start(); })(); // ...your API routes ``` Register the `plugin`. Also register [`@fastify/formbody`](https://github.com/fastify/fastify-formbody) plugin. ```tsx let fastify = fastifyImport(); // ...other middlewares // highlight-start fastify.register(cors, { origin: "", allowedHeaders: ["Content-Type", ...supertokens.getAllCORSHeaders()], credentials: true, }); // highlight-end (async () => { // highlight-next-line await fastify.register(formDataPlugin); // highlight-next-line await fastify.register(plugin); await fastify.listen({ port: 8000 }); })(); // ...your API routes ``` :::important Add the `middleware` BEFORE all your routes. ::: ```tsx let app = new Koa(); app.use( cors({ // highlight-start origin: "", allowHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], credentials: true, // highlight-end }), ); // highlight-next-line app.use(middleware()); // ...your API routes ``` :::important Add the `middleware` BEFORE all your routes. ::: ```tsx let app = new RestApplication({ rest: { cors: { // highlight-start origin: "", allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], credentials: true, // highlight-end }, }, }); // highlight-next-line app.middleware(middleware); // ...your API routes ``` Use the `supertokens.Middleware` and the `supertokens.GetAllCORSHeaders()` functions as shown below. ```go let app = express(); // ...your API routes // highlight-start // Add this AFTER all your routes app.use(errorHandler()); // highlight-end // your own error handler app.use((err: unknown, req: Request, res: Response, next: NextFunction) => { /* ... */ }); ``` No additional `errorHandler` is required. Add the `errorHandler` **Before all your routes and plugin registration** ```tsx let fastify = Fastify(); // highlight-next-line fastify.setErrorHandler(errorHandler()); // ...your API routes ``` No additional `errorHandler` is required. No additional `errorHandler` is required. :::info You can skip this step ::: :::info You can skip this step ::: ## 5. Secure Application Routes Now that your server can authenticate users, the final step that you need to take care of is to prevent unauthorized access to certain parts of the application. For your APIs that require a user to be logged in, use the `verifySession` middleware. ```tsx let app = express(); // highlight-start app.post("/like-comment", verifySession(), (req: SessionRequest, res) => { let userId = req.session!.getUserId(); // highlight-end //.... }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/like-comment", method: "post", //highlight-start options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //highlight-end //... }, }); ``` ```tsx let fastify = Fastify(); //highlight-start fastify.post( "/like-comment", { preHandler: verifySession(), }, (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //highlight-end //.... }, ); ``` ```tsx let router = new KoaRouter(); //highlight-start router.post("/like-comment", verifySession(), (ctx: SessionContext, next) => { let userId = ctx.session!.getUserId(); //highlight-end //.... }); ``` ```tsx class LikeComment { //highlight-start constructor( @inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext, ) {} @post("/like-comment") @intercept(verifySession()) @response(200) handler() { let userId = (this.ctx as SessionContext).session!.getUserId(); //highlight-end //.... } } ``` For your APIs that require a user to be logged in, use the `VerifySession` middleware. ```go # Quickstart - Example App Source: https://supertokens.com/docs/quickstart/example-applications ## Overview The fastest way to run a quick demo of **SuperTokens** is to use our cli to generate an example application. You will get a quick understanding of how the authentication experience works and the overall setup of a project. ## Project Structure The command will create a new folder based on the application name that you have specified. Inside it you will find a project configured to support an authentication flow based on the recipe that you have chosen. ## Next Steps If you want more explanations on how to integrate **SuperTokens** with your application, please check our [**Quickstart Guide**](/docs/quickstart/introduction). Otherwise, you can explore the other sections of the documentation that expand on the features of **SuperTokens** and offer you in-depth instructions on how to use them. Self Host SuperTokens Core Run the SuperTokens in your own infrastructure. Migration Guide Migrate your data from other authentication providers. Multi Factor Authentication Set up additional authentication layers in you sign in process. User Management Dashboard Administrate your users from the SuperTokens Dashboard. User Roles and Permissions Adjust the authorization settings in your application. # Quickstart - Build with AI Tools Source: https://supertokens.com/docs/quickstart/build-with-ai-tools ## Overview If you plan on using large language models (LLMs) to assist in the process of integrating SuperTokens, the documentation exposes a set of helpers that can aid you. ## Text documentation You can access all the documentation as plain text markdown files by appending `.md` to the end of any URL or by using the `Copy Markdown` button, from the top right part of each page. For example, you can find the plain text version of this page at [https://supertokens.com/docs/quickstart/build-with-ai-tools.md](https://supertokens.com/docs/quickstart/build-with-ai-tools.md). This plain text format is ideal for AI tools and agents as it: - Contains fewer formatting tokens. - Displays content that might otherwise be hidden in the HTML or JavaScript-rendered version (such as content in tabs). - Maintains a clear markdown hierarchy that LLMs can easily parse and understand. - Can be read in a paginated format using the `offset` and `length` query parameters. ### `/llms.txt` The documentation website hosts an [/llms.txt file](https://supertokens.com/docs/llms.txt) which instructs AI tools and agents on how to retrieve the plain text versions of our pages. The file follows an [emerging standard](https://llmstxt.org/) for enhancing content accessibility to LLMs. Additionally, to access the entire documentation content in a single page you can use the [/llms-full.txt file](https://supertokens.com/docs/llms-full.txt). Keep in mind that this file is really large and might get ignored by LLMs. You can use the `offset` and `length` query parameters to paginate the content. ## Model Context Protocol (MCP) Server If you want to leverage the agentic capabilities of an LLM tool you can use the **SuperTokens Model Context Protocol** (MCP) server. The server exposes a set of tools which instruct the LLM on how to access and read the documentation as well as how to administer your authentication integration. ### Installation You can use the MCP server in two ways: - over HTTP, as an endpoint exposed by your current server - as a CLI script, over STDIO #### Using HTTP ### Install the plugin ```bash npm i -s supertokens-mcp-plugin ``` ```bash yarn add supertokens-mcp-plugin ``` ```bash pnpm add supertokens-auth-react supertokens-web-js ``` ### Update the SDK initialization code Initialize and include the `SuperTokensAdminMcpServer` in your SDK configuration. You have to specify how the client will be authorized through either [claim validators](/docs/additional-verification/session-verification/claim-validation) or through custom logic defined in the `validateTokenPayload` function. ```ts const adminMcpServer = new SuperTokensAdminMcpServer({ path: "/mcp/admin", validateTokenPayload: async (accessTokenPayload) => { // Use custom logic to authenticate who can access the admin MCP server return { status: "OK" }; }, claimValidators: [UserRoleClaim.validators.includes("admin")], }); export const SuperTokensConfig = { supertokens: { connectionURI: "", apiKey: "", }, appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ // Include your existing recipes here // The OAuth2Provider recipe is required for the MCP authorization process OAuth2Provider.init(), ], // Pass the MCP server through the plguin configuration section experimental: { plugins: [ SuperTokensMcpPlugin.init({ mcpServers: [adminMcpServer], }), ], }, }; ``` ### Add the MCP server in a client configuration Add the following to your `~/.cursor/mcp.json` file. To learn more, see the [Cursor documentation](https://docs.cursor.com/context/model-context-protocol). ```json { "mcpServers": { "server-with-authentication": { "command": "npx", "args": ["mcp-remote", "/mcp"] } } } ``` Add the following to your `.vscode/mcp.json` file. To learn more, see the [VS Code documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers). ```json { "servers": { "server-with-authentication": { "command": "npx", "args": ["mcp-remote", "/mcp"] } } } ``` Add the following to your `~/.codeium/windsurf/mcp_config.json` file. To learn more, see the [Windsurf documentation](https://docs.windsurf.com/windsurf/cascade/mcp). ```json { "mcpServers": { "server-with-authentication": { "command": "npx", "args": ["mcp-remote", "/mcp"] } } } ``` Add the following to your `claude_desktop_config.json` file. To learn more, see the [Claude Desktop documentation](https://modelcontextprotocol.io/quickstart/user). ```json { "mcpServers": { "server-with-authentication": { "command": "npx", "args": ["mcp-remote", "/mcp"] } } } ``` #### Using STDIO If you plan on using the server locally you can run it directly through `npx`. You have to provide a set of environment variables that match the SDK configuration values. Add the following to your `~/.cursor/mcp.json` file. To learn more, see the [Cursor documentation](https://docs.cursor.com/context/model-context-protocol). ```json { "mcpServers": { "supertokens": { "command": "npx", "args": ["-y", "supertokens-mcp-plugin"], "env": { "APP_NAME":"", "API_DOMAIN":"", "WEBSITE_DOMAIN":"", "API_BASE_PATH":"", "WEBSITE_BASE_PATH":"", "CONNECTION_URI":"", "API_KEY":"API_KEY" } } } } ``` Add the following to your `.vscode/mcp.json` file. To learn more, see the [VS Code documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers). ```json { "servers": { "supertokens": { "command": "npx", "args": ["-y", "supertokens-mcp-plugin"], "env": { "APP_NAME":"", "API_DOMAIN":"", "WEBSITE_DOMAIN":"", "API_BASE_PATH":"", "WEBSITE_BASE_PATH":"", "CONNECTION_URI":"", "API_KEY":"API_KEY" } } } } ``` Add the following to your `~/.codeium/windsurf/mcp_config.json` file. To learn more, see the [Windsurf documentation](https://docs.windsurf.com/windsurf/cascade/mcp). ```json { "mcpServers": { "supertokens": { "command": "npx", "args": ["-y", "supertokens-mcp-plugin"], "env": { "APP_NAME":"", "API_DOMAIN":"", "WEBSITE_DOMAIN":"", "API_BASE_PATH":"", "WEBSITE_BASE_PATH":"", "CONNECTION_URI":"", "API_KEY":"API_KEY" } } } } ``` Add the following to your `claude_desktop_config.json` file. To learn more, see the [Claude Desktop documentation](https://modelcontextprotocol.io/quickstart/user). ```json { "mcpServers": { "supertokens": { "command": "npx", "args": ["-y", "supertokens-mcp-plugin"], "env": { "APP_NAME":"", "API_DOMAIN":"", "WEBSITE_DOMAIN":"", "API_BASE_PATH":"", "WEBSITE_BASE_PATH":"", "CONNECTION_URI":"", "API_KEY":"API_KEY" } } } } ``` # Quickstart - Integrations - Vercel Guide Source: https://supertokens.com/docs/quickstart/integrations/vercel This page only talks about what environment variables to use when you are deploying an application on Vercel. For a full set of instructions on how to integrate **SuperTokens** in a **Next.js** project, please see either our [app router](/docs/quickstart/integrations/nextjs/app-directory/about) or [pages router](/docs/quickstart/integrations/nextjs/app-directory/about) guides. ## Working with Vercel's inspect and production URL Vercel provides one production URL per app and one unique inspect URL per deployment. To get SuperTokens to work with dynamic URLs, you need to make the following changes to the [`appInfo` object](/docs/references/frontend-sdks/reference#sdk-configuration): ### On the frontend ```text appInfo = { apiDomain: window.location.origin, websiteDomain: window.location.origin, ... } ``` ```text appInfo = { apiDomain: window.location.origin ... } ``` ### On the backend ```text appInfo = { apiDomain: process.env.VERCEL_URL, websiteDomain: process.env.VERCEL_URL, ... }, ``` Vercel adds an environment variable to the backend - `VERCEL_URL`, which points to the current URL that the app is deployed on. This allows SuperTokens to work on all inspect URLs generated by Vercel without you having to keep changing your code. :::note The above setting works only if your backend and frontend are deployed on the same URL. If you are using a different backend and using Vercel only for your frontend, then: - Set the `apiDomain` on the frontend and backend to point to your backend. - The `websiteDomain` on the frontend should be `window.location.origin`, but on the backend, it should be equal to your production deployment URL. This will break certain features of the app for inspect URL deployments, but it will work as expected for production deployments. ::: # Quickstart - Integrations - AWS Lambda - Quickstart Guide Source: https://supertokens.com/docs/quickstart/integrations/aws-lambda/quickstart-guide The following guide shows you how to use **SuperTokens** in an AWS Lambda environment. You can also check out the [example repository](https://github.com/supertokens/supertokens-node/tree/master/examples/aws/with-emailpassword) for a full implementation. ## Before you start These instructions assume that you have completed the [quickstart guide](/docs/quickstart/frontend-setup). If not, please go through it and create the example application before you start this tutorial. ## Steps :::caution Follow the [quickstart guide](/docs/quickstart/frontend-setup) first to learn how to set up the frontend. ::: ### 1. Set up API Gateway #### 1.1 Create a REST API Gateway We will be using AWS API Gateway to create a REST API that will be used to communicate with our Lambda functions. Create API gateway step UI #### 1.2 Set up authentication routes Create a `/auth` resource and then `/auth/{proxy+}` resources. This will act as a catch-all for all SuperTokens auth routes. **Enable CORS** while creating the proxy resource. Create proxy route step UI Route creation complete step UI #### 1.3 Attach lambda to the `ANY` method of the proxy resource Click on the "ANY" method and then "Integration" to configure the lambda function. Check **Lambda proxy integration** and then select your lambda function. Configure lambda integration UI :::important Ensure that the **Lambda proxy integration** toggle is turned on. ::: #### 1.4 Enable CORS for the proxy path Click on the `{proxy+}` resource and then "Enable CORS" button to open the CORS configuration page. Enable CORS for the proxy path UI In the CORS configuration page do the following: - Select the 'Default 4XX' and 'Default 5XX' checkboxes under Gateway responses - Select the 'OPTIONS' checkbox under Methods - Add `rid,fdi-version,anti-csrf,st-auth-mode` to the existing `Access-Control-Allow-Headers` - Set `Access-Control-Allow-Origin` to `''` - Select `Access-Control-Allow-Credentials` checkbox CORS configuration page #### 1.5 Deploy the API Gateway Click the **Deploy API** button in the top right corner to deploy the API. During deployment, you'll be prompted to create a stage; for this tutorial, name the stage `dev`. After deployment, you will receive your `Invoke URL`. :::important Update `apiDomain`, `apiBasePath`, and `apiGatewayPath` in both Lambda configuration and your frontend config if they have changed post API Gateway configuration. ::: ### 2. Set up Lambda layer #### 2.1 Create Lambda layer with required libraries ```bash mkdir lambda && cd lambda npm i -s supertokens-node @middy/core @middy/http-cors mkdir nodejs && cp -r node_modules nodejs zip -r supertokens-node.zip nodejs/ ``` ```bash mkdir lambda && cd lambda pip install --target ./python fastapi uvicorn mangum nest_asyncio supertokens-python cd python && rm -r *dist-info **/__pycache__ && cd .. zip -r supertokens-python.zip python/ ``` #### 2.2 Upload SuperTokens lambda layer Open AWS Lambda dashboard and click on layers: AWS Lambda sidebar UI Click "Create Layer" button: Create layer button UI Give a name for your layer, upload the zip and select the runtime Lambda layer node configuration UI Lambda layer python configuration UI ### 3. Set up Lambda #### 3.1 Create a new lambda Click "Create Function" in the AWS Lambda dashboard, enter the function name and runtime, and create your Lambda function. Create new Lambda configurations UI Node Create new Lambda configurations UI Python #### 3.2 Link lambda layer with the lambda function Scroll to the bottom and look for the `Layers` tab. Click on `Add a layer` Link Lambda function with the Lambda layer Select `Custom Layer` and then select the layer we created in the first step: Link custom layer with Lambda function Node Link custom layer with Lambda function Python #### 3.3 Create a backend config file Using the editor provided by AWS, create a new config file and write the following code: ```tsx title="config.mjs" showAppTypeSelect export function getBackendConfig() { return { framework: "awsLambda", supertokens: { connectionURI: "", // apiKey: "^{coreInfo.key}", }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", apiGatewayPath: "/dev" }, recipeList: [ EmailPassword.init(), Session.init(), ], isInServerlessEnv: true, } } ``` ```python title="config.py" showAppTypeSelect from supertokens_python.recipe import emailpassword, session from supertokens_python import SupertokensConfig, InputAppInfo supertokens_config = SupertokensConfig( connection_uri="", # api_key="^{coreInfo.key}" ) app_info = InputAppInfo( # learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration app_name="", api_domain="", website_domain="", api_base_path="", website_base_path="", api_gateway_path="/dev", ) framework = "fastapi" recipe_list = [ session.init(), emailpassword.init(), ] ``` :::important In the above code, notice the extra config of `apiGatewayPath` that was added to the `appInfo` object. The value of this should be whatever you have set as the value of your [AWS stage](https://docs.aws.amazon.com/apigateway/latest/developerguide/stages.html) which scopes your API endpoints. For example, you may have a stage name per development environment: - One for development (`/dev`). - One for testing (`/test`). - One for prod (`/prod`). So the value of `apiGatewayPath` should be set according to the above based on the environment it's running under. You also need to change the `apiBasePath` on the frontend config to append the stage to the path. For example, if the frontend is query the development stage and the value of `apiBasePath` is `/auth`, you should change it to `/dev/auth`. ::: :::note You may edit the `apiBasePath` and `apiGatewayPath` value later if you haven't setup the API Gateway yet. ::: #### 3.4 Add the SuperTokens auth middleware Using the editor provided by AWS, create/replace the handler file contents with the following code: ```tsx title="index.mjs" // @ts-ignore supertokens.init(getBackendConfig()); export const handler = middy( // @ts-ignore middleware((event) => { // SuperTokens middleware didn't handle the route, return your custom response return { body: JSON.stringify({ msg: "Hello!", }), statusCode: 200, }; }) ) .use( cors({ origin: getBackendConfig().appInfo.websiteDomain, credentials: true, headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), methods: "OPTIONS,POST,GET,PUT,DELETE", }) ) .onError((request) => { throw request.error; }); ``` Add SuperTokens auth middleware UI
:::important Since, we are using `esm` imports, we will need to set `NODE_OPTIONS="--experimental-specifier-resolution=node"` flag in the lambda environment variables. See the [Node.js](https://nodejs.org/docs/latest-v16.x/api/esm.html#customizing-esm-specifier-resolution-algorithm) documentation for more information. Configuring environment variables UI :::
```python title="handler.py" // @ts-ignore supertokens.init(getBackendConfig()); const httpHandler = middy( // @ts-ignore middleware((event) => { // SuperTokens middleware didn't handle the route, return your custom response return { body: JSON.stringify({ msg: "Hello!", }), statusCode: 200, }; }) ) .use( cors({ origin: getBackendConfig().appInfo.websiteDomain, credentials: true, headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), methods: "OPTIONS,POST,GET,PUT,DELETE", }) ) .onError((request) => { throw request.error; }); // @ts-expect-error const postAuth = async (event, context) => { // Plugins generally inject a `source` property in the event object. if (event.source === 'serverless-plugin-warmup') { console.info('postAuth 010: warming up lambda. Bypassing authMiddleware.'); return { statusCode: 200, body: JSON.stringify({ message: 'Warm-up successful' }), }; } return httpHandler(event, context); }; export const handler = postAuth; ``` # Quickstart - Integrations - AWS Lambda - Session verification Source: https://supertokens.com/docs/quickstart/integrations/aws-lambda/session-verification The following page shows you three different ways to verify sessions in a lambda integration. Choose the one that works best based on the particularities of your use case. :::caution This guide only applies to scenarios which involve **SuperTokens Session Access Tokens**. If you are implementing either, [**Unified Login**](/docs/authentication/unified-login/introduction) or [**Microservice Authentication**](/docs/authentication/m2m/introduction), features that make use of **OAuth2 Access Tokens**, please check the [separate page](/docs/authentication/unified-login/verify-tokens) that shows you how to verify those types of tokens. ::: ## Using Session Verification When building your own APIs, you may need to verify the session of the user before proceeding further. SuperTokens SDK exposes a `verifySession` function that can be utilized for this. In this guide, we will be creating a `/user` `GET` route that will return the current session information. ### 1. Add `/user` `GET` route in your API Gateway Create a `/user` resource and then `GET` method in your API Gateway. Configure the lambda integration and CORS just like we did [for the auth routes](/docs/quickstart/integrations/aws-lambda/quickstart-guide#13-attach-lambda-to-the-any-method-of-the-proxy-resource). ### 2. Create a file in your lambda to handle the `/user` route. An example of this is [here](https://github.com/supertokens/supertokens-node/blob/master/examples/aws/with-emailpassword/backend/user.mjs). ```tsx title="user.mjs" // @ts-ignore supertokens.init(getBackendConfig()); type AuthorizerEvent = SessionEvent & APIGatewayAuthorizerEvent; const lambdaHandler = async (event: AuthorizerEvent) => { return { body: JSON.stringify({ sessionHandle: event.session?.getHandle(), userId: event.session?.getUserId(), accessTokenPayload: event.session?.getAccessTokenPayload(), }), statusCode: 200, }; }; export const handler = middy(verifySession(lambdaHandler)) .use( cors({ origin: getBackendConfig().appInfo.websiteDomain, credentials: true, headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), methods: "OPTIONS,POST,GET,PUT,DELETE", }) ) .onError((request) => { throw request.error; }); ``` Now, import this function in your `index.mjs` handler file as shown below: ```tsx title="index.mjs" // @ts-ignore // highlight-start // @ts-ignore // highlight-end supertokens.init(getBackendConfig()); export const handler = middy( middleware((event) => { // highlight-start if (event.path === "/user") { return userHandler(event); // highlight-end } return { body: JSON.stringify({ msg: "Hello!", }), statusCode: 200, }; }) ) .use( cors({ origin: getBackendConfig().appInfo.websiteDomain, credentials: true, headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), methods: "OPTIONS,POST,GET,PUT,DELETE", }) ) .onError((request) => { throw request.error; }); ``` :::note The `verifySession` middleware automatically returns a 401 Unauthorised error if the session is not valid. You can alter the default behaviour by passing `{ sessionRequired: false }` as the second argument to the `verifySession` middleware. If each API route has its own lambda function, you can skip using the SuperTokens auth middleware. Instead, ensure to call `supertokens.init` and include the `Session` recipe in the `recipeList` for each respective lambda function. ::: ```python title="handler.py" ```python title="auth.py" We can then use `getSession()` to get the session. ```tsx title="index.mjs" // @ts-expect-error supertokens.init(getBackendConfig()); type AuthorizerEvent = SessionEvent & APIGatewayAuthorizerEvent; export const handler = async function (event: AuthorizerEvent) { try { const session = await Session.getSession(event, event, { sessionRequired: false }); if (session) { return generateAllow(session.getUserId(), event.methodArn, { setCookie: event.supertokens.response.cookies.join(', '), }); } else { return generateAllow("", event.methodArn, { setCookie: event.supertokens.response.cookies.join(', '), }); } } catch (ex: any) { if (ex.type === "TRY_REFRESH_TOKEN" || ex.type === "UNAUTHORISED") { throw new Error("Unauthorized"); } if (ex.type === "INVALID_CLAIMS") { return generateDeny("", event.methodArn, { body: JSON.stringify({ message: "invalid claim", claimValidationErrors: ex.payload, }), setCookie: event.supertokens.response.cookies.join(", "), }); } throw ex; } } const generatePolicy = function (principalId: string, effect: StatementEffect, resource: string, context?: any) { const policyDocument: PolicyDocument = { Version: '2012-10-17', Statement: [], }; const statementOne: Statement = { Action: 'execute-api:Invoke', Effect: effect, Resource: resource, }; policyDocument.Statement[0] = statementOne; const authResponse: AuthResponse = { principalId: principalId, policyDocument: policyDocument, context, }; return authResponse; } const generateAllow = function (principalId: string, resource: string, context?: any) { return generatePolicy(principalId, 'Allow', resource, context); }; const generateDeny = function (principalId: string, resource: string, context?: any) { return generatePolicy(principalId, 'Deny', resource, context); }; ``` ### 3. Configure the Authorizer - Go to the `Authorizers` tab in the API Gateway configuration - Click **Create new Authorizer** and add it - Fill the name field - Set "Lambda function" to the one created above - Set "Lambda Event Payload" to Request - Delete the empty "Identity Source" - Click "Create" ### 4. Configure API Gateway - In your API Gateway, create the resources and methods you require, enabling CORS if necessary (see [setup API gateway](/docs/quickstart/integrations/aws-lambda/quickstart-guide#1-set-up-api-gateway) for details) - Select each method you want to enable the Authorizer and configure it to use the new `Authorizer` - Click on "Method Request" - Edit the "Authorization" field in Settings and set it to the one we just created. - Go back to the method configuration and click on "Integration Request" - Set up the integration you require (see [AppSync](/docs/quickstart/integrations/aws-lambda/appsync-integration) for an example) - Add a header mapping to make use of the context set in the lambda. - Open "HTTP Headers" - Add all headers required (e.g., "x-user-id" mapped to "context.authorizer.principalId") - Repeat for any values from the context you want to add as a Header - Go back to the method configuration and click on "Method Response" - Open the dropdown next to the 200 status code - Add the "Set-Cookie" header - Add any other headers that should be present on the response. - Go back to the method configuration and click on "Integration Response" - Open the dropdown - Open "Header Mappings" - Add "Set-Cookie" mapped to "context.authorizer.setCookie" - In the API Gateway left menu, select "Gateway Responses" - Select "Access Denied" - Click "Edit" - Add response headers: - Add `Access-Control-Allow-Origin` with value `''` - Add `Access-Control-Allow-Credentials` with value `'true'`. **Don't miss out on those quotes else it won't get configured correctly.** - Add "Set-Cookie" with value `context.authorizer.setCookie` **no quotes** - Under response templates: - Select `application/json`: - Set "Response template body" to `$context.authorizer.body` - Click "Save" - Select "Unauthorized" - Add response headers: - Add `Access-Control-Allow-Origin` with value `''` - Add `Access-Control-Allow-Credentials` with value `'true'`. **Don't miss out on those quotes else it won't get configured correctly.** - Click "Save" - Deploy your API and test it --- ## Using JWT Authorizers :::caution AWS supports JWT authorizers for HTTP APIs and not REST APIs on the API Gateway service. For REST APIs follow the [Lambda authorizer](/docs/quickstart/integrations/aws-lambda/session-verification#using-lambda-authorizers) guide This guide will work if you are using **SuperTokens Session Tokens**. If you implementing an **OAuth2** setup, through the [**Unified Login**](/docs/authentication/unified-login/introduction) or the [**Microservice Authentication**](/docs/authentication/m2m/client-credentials) features, you will have to manually set the token audience property. Please check the referenced pages for more information. ::: ### 1. Add the `aud` claim in the JWT based on the authorizer configuration ```tsx title="config.mjs" showAppTypeSelect export function getBackendConfig() { return { framework: "awsLambda", supertokens: { connectionURI: "", apiKey: "^{coreInfo.key}", }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ // highlight-start Session.init({ exposeAccessTokenToFrontendInCookieBasedAuth: true, override: { functions: function (originalImplementation) { return { ...originalImplementation, createNewSession: async function (input) { input.accessTokenPayload = { ...input.accessTokenPayload, /* * AWS requires JWTs to contain an audience (aud) claim * The value for this claim should be the same * as the value you set when creating the * authorizer */ aud: "jwtAuthorizers", }; return originalImplementation.createNewSession(input); }, }; } }, }), ], // highlight-end isInServerlessEnv: true, } } ``` ```python title="config.py" showAppTypeSelect from supertokens_python.recipe import session from supertokens_python import ( InputAppInfo, SupertokensConfig, ) from supertokens_python.recipe.session.interfaces import RecipeInterface as SessionRecipeInterface from typing import Any, Dict, Optional from supertokens_python.types import RecipeUserId supertokens_config = SupertokensConfig( connection_uri="", api_key="^{coreInfo.key}" ) app_info = InputAppInfo( # learn more about this on https://supertokens.com/docs/session/appinfo app_name="", api_domain="", website_domain="", api_base_path="", website_base_path="", api_gateway_path="/dev", ) framework = "fastapi" # highlight-start def override_session_functions(oi: SessionRecipeInterface) -> SessionRecipeInterface: oi_create_new_session = oi.create_new_session async def create_new_session( user_id: str, recipe_user_id: RecipeUserId, access_token_payload: Optional[Dict[str, Any]], session_data_in_database: Optional[Dict[str, Any]], disable_anti_csrf: Optional[bool], tenant_id: str, user_context: Dict[str, Any], ): # AWS requires JWTs to contain an audience (aud) claim # The value for this claim should be the same as the # value you set when creating the authorizer # highlight-next-line if access_token_payload is None: access_token_payload = {} access_token_payload["aud"] = "jwtAuthorizers" return await oi_create_new_session(user_id, recipe_user_id, access_token_payload, session_data_in_database, disable_anti_csrf, tenant_id, user_context) oi.create_new_session = create_new_session return oi # highlight-end recipe_list = [ # highlight-start session.init( override=session.InputOverrideConfig( functions=override_session_functions, ), expose_access_token_to_frontend_in_cookie_based_auth=True, ), # highlight-end ] ``` ### 2. Configure your authorizer - Go to the "Authorizers" tab in the API Gateway configuration and select the "Manage authorizers" tab - Click "Create", in the creation screen select "JWT" as the "Authorizer type" - Enter a name for your authorizer (You can enter any name for this field) - Use `$request.header.Authorization` for the "Identity source". This means that API requests will contain the JWT as a Bearer token under the request header "Authorization". - Use `//` for the "Issuer URL". - Set a value for the "Audience" field, this will be the value you expect the JWT to have under the `aud` claim. In the backend config above the value is set to `"jwtAuthorizers"` ### 3. Add the authorizer to your API - In the "Authorization" section select the "Attach authorizers to routes" tab - Click on the route you want to add the authorizer to and select the authorizer you created from the dropdown - Click "Attach authorizer" - Deploy your changes and test your API ### 4. Check for auth claims of the JWT Once the JWT authorizer successfully validates the JWT, the claims of the JWT will be available to your lambda functions via `$event.requestContext.authorizer.jwt.claims`. You should check for the right authorization access here. For example, if one of your lambda functions requires that the user's email is verified, then it should check for the `jwt` payload's `st-ev` claim value to be `{v: true, t:...}`, else it should reject the request. Similar checks need to be done to enforce the right user role or if 2FA is completed or not. This is required because SuperTokens issues JWTs immediately after the user signs up / logs in, regardless of if all the authorisation checks pass or not. Functions exposed by our SDK like `verifySession` or `getSession` do these authorisation checks on their own, but since these functions are not used in the this flow, you will have to check them on your own. # Quickstart - Integrations - AWS Lambda - AppSync integration Source: https://supertokens.com/docs/quickstart/integrations/aws-lambda/appsync-integration ## Overview A Lambda Authorizer configured like in the [Authorizer guide](/docs/quickstart/integrations/aws-lambda/session-verification#using-lambda-authorizers) can help integrate SuperTokens with an AppSync service. ## Before you start The following steps assume that you already have configured **SuperTokens** in AWS Lambda. If not, please refer to the [AWS Lambda integration guide](/docs/quickstart/integrations/aws-lambda/quickstart-guide). ## Steps ### 1. Set up the AppSync service Set up the AppSync service with an API key authorization. For more details, please see the [AWS documentation](https://docs.aws.amazon.com/appsync/latest/devguide/quickstart.html). ### 2. Configure the API Gateway with the authorizer Follow the [Authorizer guide](/docs/quickstart/integrations/aws-lambda/session-verification#using-lambda-authorizers) to set up the API Gateway with the `/auth`, and `/graphql` resources set up. `/auth` should be pointed to a lambda that handles the auth APIs. When setting up the POST method on `/graphql`, you should use the following settings: - Integration type: AWS service - AWS Region: the region of the AppSync service - AWS Service: AppSync Data Plane - AWS Subdomain: the part of the domain of the GraphQL service before `.appsync-api.` - HTTP method: POST - Action type: Use path override - Path override: `/graphql` - Execution role: the ARN of an execution role that is authorized to call the AppSync service (e.g.: `AWSAppSyncInvokeFullAccess`) ### 3. Set up the integration headers Configure the "Integration Request" of the `/graphql` POST method. - Add `HTTP Header` mappings: - "x-api-key": The API key of the App Sync service, wrapped in single quotes. - "x-user-id": `context.authorizer.principalId`, without quotes. ### 4. Consume the context in resolvers You can access the headers you mapped above in resolvers through the context. (e.g., $context.request.headers.custom) For more information, please see the [resolver context](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html) docs. # Quickstart - Integrations - GraphQL Session Verification Source: https://supertokens.com/docs/quickstart/integrations/graphql ## Before you start These instructions only show you how to perform **session verification** in a GraphQL context. You will first have to go through the [quickstart guide](/docs/quickstart/frontend-setup) to configure **SuperTokens** and then return to this page. :::caution This guide only applies to scenarios which involve **SuperTokens Session Access Tokens**. If you are implementing either, [**Unified Login**](/docs/authentication/unified-login/introduction) or [**Microservice Authentication**](/docs/authentication/m2m/introduction), features that make use of **OAuth2 Access Tokens**, please check the [separate page](/docs/authentication/unified-login/verify-tokens) that shows you how to verify those types of tokens. ::: ## Using the GraphQL context We want to use the `Session.getSession` function in the `context` function to verify the session, and add the `userId` into our context so that our resolvers can read it. If the user id not logged in, we will set the `userId` to `undefined` in the context ```tsx let app = express(); const typeDefs = '...' const resolvers = {/* ... */ } const server = new ApolloServer({ typeDefs, resolvers, }) server.start().then(() => { app.use(express.json(), expressMiddleware(server, { // Note: This example uses the `req` and `res` argument to access headers, // but the arguments received by `context` vary by integration. // This means they vary for Express, Fastify, Lambda, etc. context: async ({ req, res }) => { // highlight-start try { let session = await Session.getSession(req, res, { sessionRequired: false }) return { userId: session !== undefined ? session.getUserId() : undefined }; } catch (err) { if (Session.Error.isErrorFromSuperTokens(err)) { throw new GraphQLError('Session related error', { extensions: { code: 'UNAUTHENTICATED', http: { status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401 }, }, }); } throw err; } // highlight-end }, })) app.listen(3001, () => { console.log("Server started"); }) }) ``` In the above code snippet, we first attempt to verify the session using the `Session.getSession` function. If the session is valid, we will add the `userId` to the context. If the access token has expired, we will throw an error with a status code of `401`. If a session claim has failed (for example if the user's email is not verified) we will return a status code of `403`. The `401` status code will cause the session refresh flow to start, which will give a new access token to the user, or else if the session was revoked, the user will be logged out. In case the user is not logged in, the `Session.getSession` function will throw return `undefined`, in which case, your resolvers will not have a `userId` in the context. The downside of this method is that if you want to mutate the session's access token payload in one of your resolvers, then you don't have access to the `session` object in there. This is where the method below comes into the picture: ## Using the GraphQL resolver Unlike the method above, we will be doing session verification on a per resolver basis here. This means that you will have access to the `session` object in your resolver using which you can update the information in the session (like its access token payload). We start by creating a helper function (a sort of middleware for your resolver) which you will have to call in all of your resolvers that require a session: ```tsx async function withSession(contextValue: any, resolver: (session: SessionContainer) => Promise) { try { let session = await Session.getSession(contextValue.req, contextValue.res); return await resolver(session); } catch (err) { if (Session.Error.isErrorFromSuperTokens(err)) { throw new GraphQLError('Session related error', { extensions: { code: 'UNAUTHENTICATED', http: { status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401 }, }, }); } } } ``` In the above function, we attempt to verify the session using `Session.getSession`. If the session is valid, we will call the `resolver` function with the `session` object. If the access token has expired, or if the session does not exist, we will throw an error with a status code of `401`. If a session claim has failed (for example if the user's email is not verified) we will return a status code of `403`. For this resolver to work, we will have to add the `req` and `res` object into the GraphQL context. This can be done as follows: ```tsx let app = express(); const typeDefs = '...' const resolvers = {/* ... */} const server = new ApolloServer({ typeDefs, resolvers, }) server.start().then(() => { app.use(express.json(), expressMiddleware(server, { // Note: This example uses the `req` and `res` argument to access headers, // but the arguments received by `context` vary by integration. // This means they vary for Express, Fastify, Lambda, etc. context: async ({req, res}) => { // highlight-start return { req, res }; // highlight-end }, })) app.listen(3001, () => { console.log("Server started"); }) }) ``` Finally, we can use our `withSession` in our resolvers as shown below: ```tsx declare let getUserName: any; // REMOVE_FROM_OUTPUT declare let withSession: (contextValue: any, func: (sessoin: any) => Promise) => Promise; // REMOVE_FROM_OUTPUT declare let typeDefs: any; // REMOVE_FROM_OUTPUT const server = new ApolloServer({ typeDefs, resolvers: { Query: { userProfile: async (_: any, __: any, contextValue) => { // highlight-start // starts of your resolver code.. return await withSession(contextValue, async (session) => { // getUserName is a custom application function... let name = await getUserName(session.getUserId()) return { userId: session.getUserId(), sessionHandle: session.getHandle(), name }; }); // highlight-end } }, }, }) ``` # Quickstart - Integrations - Hasura Guide Source: https://supertokens.com/docs/quickstart/integrations/hasura ## Before you start The tutorial assumes that you already have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). Using SuperTokens with Hasura requires you to host your own API layer that uses our Backend SDK. If you do not want to host your own server you can use a serverless environment to achieve this. :::caution This guide only applies to scenarios which involve **SuperTokens Session Access Tokens**. If you are implementing either, [**Unified Login**](/docs/authentication/unified-login/introduction) or [**Microservice Authentication**](/docs/authentication/m2m/introduction), features that make use of **OAuth2 Access Tokens**, please check the [separate page](/docs/authentication/unified-login/verify-tokens) that shows you how to verify those types of tokens. ::: ## Steps ### 1. Expose the access token to the frontend For cookie based auth, the access token is not available on the frontend by default. In order to expose it, you need to set the `exposeAccessTokenToFrontendInCookieBasedAuth` config to `true`. ```tsx SuperTokens.init({ supertokens: { connectionURI: "..." }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ //highlight-start exposeAccessTokenToFrontendInCookieBasedAuth: true //highlight-end }) ] }); ``` ```go SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ exposeAccessTokenToFrontendInCookieBasedAuth: true, override: { functions: function (originalImplementation) { return { ...originalImplementation, createNewSession: async function (input) { //highlight-start input.accessTokenPayload = { ...input.accessTokenPayload, "https://hasura.io/jwt/claims": { "x-hasura-user-id": input.userId, "x-hasura-default-role": "user", "x-hasura-allowed-roles": ["user"], } }; //highlight-end return originalImplementation.createNewSession(input); }, }; } }, }) ] }); ``` ```go async function getToken(): Promise { // highlight-next-line const accessToken = await Session.getAccessToken(); console.log(accessToken); } ``` ```tsx async function getToken(): Promise { // highlight-next-line const accessToken = await supertokensSession.getAccessToken(); console.log(accessToken); } ``` ```tsx async function getToken(): Promise { // highlight-next-line const accessToken = await SuperTokens.getAccessToken(); console.log(accessToken); } ``` ```kotlin Future getToken() async { return await SuperTokens.getAccessToken(); } ```
#### 5.2 Make an HTTP requests ```tsx async function makeRequest() { let url = "..."; let jwt = "..."; // Refer to step 5.a let response = await axios.get(url, { // highlight-start headers: { "Authorization": `Bearer ${jwt}`, }, // highlight-end }); } ``` ## Local development If you are using Hasura cloud and testing your backend APIs in your local environment, JWT verification will fail because Hasura will not be able to query the JWKS endpoint (because the cloud can not query your local environment i.e localhost, 127.0.0.1). To solve this problem you will need to expose your locally hosted backend APIs to the internet. For example you can use [ngrok](https://ngrok.com/). After that, you need to configure Hasura to use the `//jwt/jwks.json` as the JWKS endpoint (explained in [step 4](#4-configure-hasura-environment-variables)) # Quickstart - Integrations - NestJS guide Source: https://supertokens.com/docs/quickstart/integrations/nestjs ## Overview Integrating SuperTokens into a NestJS backend differs in some aspects from the main quickstart guide. That's because of the additional framework specific entities that are involved. To aid the process you can use the `supertokens-nestjs` package which exposes abstractions that speed up the setup. ## Before you start This guide assumes that you have already completed the [main quickstart guide](/docs/quickstart/introduction). If not, please go through it before continuing with this page. You need to first understand how to configure the required recipes and run a sample project. You can also explore the [example projects](https://github.com/supertokens/supertokens-nestjs/tree/main/examples) for complete code references on how to use the libraries. ## Steps ### 1. Install the required packages ```bash npm i -s supertokens-node supertokens-nestjs ``` ### 2. Initialize the `SuperTokensModule` Inside your main application module, initialize the **SuperTokensModule** with your required configuration. ```tsx @Module({ imports: [ SuperTokensModule.forRoot({ // Choose between 'express' and 'fastify' // If you are using fastify make sure to also set the fastifyAdapter property framework: 'express', supertokens: { connectionURI: '...', }, appInfo: { appName: '...', apiDomain: '...', websiteDomain: '...', }, recipeList: [ /* ... */ ], }), ], controllers: [ /* ... */ ], providers: [ /* ... */ ], }) export class AppModule {} ``` :::info Tip You can use the `SuperTokensModule.forRootAsync` if you want to load the configuration asynchronously. ::: ### 3. Update the `bootstrap` function Inside your `bootstrap` function, you have to update the CORS configuration and set the exception filter. **SuperTokens** generates a set of CORS headers that the authentication flow requires. And, the global filter ensures that all authentication related errors get handled by the SDK. ```tsx // @ts-expect-error // @ts-expect-error async function bootstrap() { const app = await NestFactory.create(AppModule) app.enableCors({ origin: [appInfo.websiteDomain], allowedHeaders: ['content-type', ...supertokens.getAllCORSHeaders()], credentials: true, }) app.useGlobalFilters(new SuperTokensExceptionFilter()) await app.listen(3001) } ``` ### 4. Add the `SuperTokensAuthGuard` The `SuperTokensAuthGuard` automatically marks the routes that it targets as protected. By default session validation gets performed based on the default configuration provided in the `Session.init` call. You can customize the validation logic with decorators. More on that in the next step. #### As a global guard This applies the `SuperTokensAuthGuard` to all routes in exposed by controllers registered in that module. ```tsx @Module({ imports: [ /* ... */ ], controllers: [ /* ... */ ], providers: [ { provide: APP_GUARD, useClass: SuperTokensAuthGuard, }, ], }) export class AppModule {} ``` #### As a controller guard This applies the `SuperTokensAuthGuard` only to the routes defined in the controller. ```tsx @Controller() @UseGuards(SuperTokensAuthGuard) export class AppController {} ``` ### 5. Manage authentication with decorators The `supertokens-nestjs` package exposes two sets of decorators: - Function decorators like `VerifySession` and `PublicAccess` that you can use on controller methods to customize the session validation logic. - Parameter decorators like `Session` that you can use to access the session data in your controller methods. ```tsx @Controller() class AppController { @Get('/user') @VerifySession() async getUserInfo(@Session('userId') userId: string) {} @Get('/user/:userId') @VerifySession({ roles: ['admin'], }) async deleteUser(@Session() session: SessionContainer) {} @Get('/user/profile') @PublicAccess() async getUserProfile() {} } ``` :::info tip With the `VerifySession` decorator, you can specify the following options: | Option | Type | Description | |--------|------|-------------| | `roles` | `string[]` | Roles that the user must have to access the route | | `permissions` | `string[]` | Permissions that the user must have to access the route | | `requiresMfa` | `boolean` | Indicates whether the user must have MFA enabled to access the route | | `requireEmailVerification` | `boolean` | Indicates whether the user must have their email verified to access the route | | `options` | `VerifySessionOptions` | The value that normally passed to the `getSession` or `verifySession` functions. Use it if you want additional levels of customization. | ::: ### 6. Configure SuperTokens core { return ( Are you using https://try.supertokens.com as the connection URI in the init function? ) }} defaultAnswer="Yes"> You need to setup an instance of the SuperTokens core for your app (that your backend should connect to). You have two options: - [Managed service](/docs/quickstart/next-steps#configure-the-core-service) - [Self hosted](/docs/deployment/self-host-supertokens) :::success You have successfully completed the quick setup! Head over to the "Post login operations" or "Common customizations" section. ::: # Quickstart - Integrations - Netlify Guide Source: https://supertokens.com/docs/quickstart/integrations/netlify ## Overview The following guide gets you though how to add SuperTokens to a Netlify serverless API. You can also check out the [example repository](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-netlify) for a full working example. ## Before you start This guide assumes that you are using Netlify for hosting your serverless API functions. If this is not the case, and you are only hosting your frontend using Netlify, please follow the [Quick setup guide](/docs/quickstart/frontend-setup) instead. ## Steps ### 1. Setup the frontend Follow the [initial quickstart guide](/docs/quickstart/frontend-setup) to configure the frontend. ### 2. Setup the backend #### 2.1 Install the SuperTokens node package ```bash npm i supertokens-node ``` #### 2.2 Create a configuration file Create a `config` folder in the root directory of your project. Create a `supertokensConfig.js` inside the `config` folder. An example of this file can be found [here](https://github.com/supertokens/supertokens-auth-react/blob/master/examples/with-netlify/config/supertokensConfig.js). #### 2.3 Create a backend configuration function ```tsx title="/config/supertokensConfig.ts" showAppTypeSelect function getBackendConfig() { return { framework: "awsLambda", supertokens: { connectionURI: "", apiKey: "^{coreInfo.key}", }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ EmailPassword.init(), Session.init(), ], isInServerlessEnv: true, } } module.exports.getBackendConfig = getBackendConfig; ``` ### 3. Expose the authentication APIs We will add all the backend APIs for auth on `/.netlify/functions/auth/*`. This can be changed by setting the `apiBasePath` property in the `appInfo` object on the backend and frontend. For the rest of this page, we will assume you are using `/.netlify/functions/auth/*`. #### 3.1 Create the `netlify/functions/auth.js` page Be sure to create the `netlify/functions/` folder. An example of this can be found [here](https://github.com/supertokens/supertokens-auth-react/blob/master/examples/with-netlify/netlify/functions/auth.js). ```tsx title="netlify/functions/auth.ts" // @ts-ignore supertokens.init(getBackendConfig()); module.exports.handler = middy(middleware(async (event, context) => { if (event.httpMethod === "OPTIONS") { return { statusCode: 200, body: "" } } return { statusCode: 404, body: "Not Found", } })).use( cors({ origin: getBackendConfig().appInfo.websiteDomain, credentials: true, headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), methods: "OPTIONS,POST,GET,PUT,DELETE", }) ).onError(request => { throw request.error; }); ``` :::important - Notice that we called `supertokens.init` above. We will need to call this in all API endpoints that use any functions related to SuperTokens. - `CORS` is only needed if you are hosting your frontend using a separate domain (if your website domain is different that your API's domain). ::: #### 3.2 Use the login widget If you are now able to sign in or sign up, this means the backend setup is done correctly! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) ### 4. Add session verification :::caution This guide only applies to scenarios which involve **SuperTokens Session Access Tokens**. If you are implementing either, [**Unified Login**](/docs/authentication/unified-login/introduction) or [**Microservice Authentication**](/docs/authentication/m2m/introduction), features that make use of **OAuth2 Access Tokens**, please check the [separate page](/docs/authentication/unified-login/verify-tokens) that shows you how to verify those types of tokens. ::: For this guide, we will assume that we want an API `/.netlify/functions/user GET` which returns the current session information. #### 4.1 Create a new file `netlify/functions/user.js` An example of this is [here](https://github.com/supertokens/supertokens-auth-react/blob/master/examples/with-netlify/netlify/functions/user.js). #### 4.2 Call the `supertokens.init` function Remember that whenever we want to use any functions from the `supertokens-node` lib, we have to call the `supertokens.init` function at the top of that serverless function file. ```tsx title="netlify/functions/user.ts" // @ts-ignore supertokens.init(getBackendConfig()) ``` #### 4.3 Use session verification with your API handler We use the `verifySession()` middleware to verify a session. ```tsx title="netlify/functions/user.ts" // @ts-ignore supertokens.init(getBackendConfig()); const handler = async (event: SessionEvent) => { return { body: JSON.stringify({ sessionHandle: event.session!.getHandle(), userId: event.session!.getUserId(), accessTokenPayload: event.session!.getAccessTokenPayload(), }), }; }; module.exports.handler = middy(verifySession(handler)).use( cors({ origin: getBackendConfig().appInfo.websiteDomain, credentials: true, headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), methods: "OPTIONS,POST,GET,PUT,DELETE", }) ).onError(request => { throw request.error; }); ``` # Quickstart - Integrations - Supabase Guide Source: https://supertokens.com/docs/quickstart/integrations/supabase ## Overview The following guide shows you how to integrate a Next.js app with SuperTokens and Supabase. It includes instructions on how to: - Create a Supabase project with a table to store your user data - Create a Supabase JWT and store the user's session - Enable row level security policies in your Supabase table to ensure only authorized users can access their data In this example, the user's email is stored mapped to their SuperTokens userId in Supabase. You can also check an [example repository](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-supabase) for specific references. ## Before you start The guide does not include instructions on how to setup a Next.js app with SuperTokens. To do this you can follow the [app router](/docs/quickstart/integrations/nextjs/app-directory/about) or [pages router](/docs/quickstart/integrations/nextjs/app-directory/about) instructions. ## Steps ### 1. Configure Supabase Supabase provides a database with authentication and authorization features. This guide uses Supabase to store the user's info mapped to their SuperTokens `userId`. #### 1.1 Create a new Supabase project 1. From your [Supabase dashboard](https://app.supabase.com/), click New project. 2. Enter a Name for your Supabase project. 3. Enter a secure Database Password. 4. Select the same Region you host your app's backend in. 5. Click Create new project. ![Supabase dashboard](/img/thirdpartyemailpassword/supabase/supabase_dashboard_create.png) #### 1.2 Create the user table in Supabase 1. From the sidebar menu in the [Supabase dashboard](https://app.supabase.com/), click Table editor, then New table. 2. Enter `users` as the `Name` field. 3. Select `Enable Row Level Security (RLS).` 4. Remove the default columns 5. Create two new columns: - `user_id` as `varchar` as primary key - email as `varchar` 6. Click `Save` to create the new table. ![Supabase table create](/img/thirdpartyemailpassword/supabase/supabase_table_create.png) ### 2. Setup JWT creation In this section, the SuperTokens backend is overridden to create a JWT signed with Supabase's secret which contains the user's `userId`. This token is used on the frontend and backend to read and write to Supabase's database. #### 2.1 Integrate your Next.js app with SuperTokens Follow either the [app router](/docs/quickstart/integrations/nextjs/app-directory/about) or [pages router](/docs/quickstart/integrations/nextjs/app-directory/about) guides for instructions on how to configure your application. #### 2.2 Save the Supabase configuration values Retrieve the Supabase configuration values from the dashboard and add them to your `.env` file: ```bash // retrieve the following from your supabase dashboard NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_KEY= SUPABASE_SIGNING_SECRET= ``` #### 2.3 Create the Supabase JWT In the Next.js app when a user signs up, you'll want to store the user's email in Supabase. The email can then be retrieved from Supabase and displayed on the frontend. To use the Supabase client to query the database, you need to create a JWT signed with your Supabase app's signing secret. This JWT also needs to contain the user's `userId` so Supabase knows an authorized user is making the request. To create this flow, SuperTokens needs to be modified so that, when a user signs up or signs in, a JWT signed with Supabase's signing secret is created and attached to the user's session. Attaching the JWT to the user's session allows the Supabase JWT to be retrieved on the frontend and backend (post session verification), which can then be used to query Supabase. To create the JWT signed with Supabase's signing secret, the `jsonwebtoken` library is used. ```bash npm install jsonwebtoken ``` The JWT can be added to the user's session by overriding the `createNewSession` function and adding it to the `accessTokenPayload` ```ts // config/backendConfig.ts let appInfo: AppInfo = { appName: "TODO: add your app name", apiDomain: "TODO: add your website domain", websiteDomain: "TODO: add your website domain" } let supabase_signing_secret = process.env.SUPABASE_SIGNING_SECRET || "TODO: Your Supabase Signing Secret"; let backendConfig = (): TypeInput => { return { framework: "express", supertokens: { connectionURI: "https://try.supertokens.com", }, appInfo, recipeList: [ // @ts-ignore EmailPassword.init({/*...*/}), SessionNode.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, createNewSession: async function (input) { const payload = { userId: input.userId, exp: Math.floor(Date.now() / 1000) + 60 * 60, }; const supabase_jwt_token = jwt.sign(payload, supabase_signing_secret); input.accessTokenPayload = { ...input.accessTokenPayload, supabase_token: supabase_jwt_token, }; return await originalImplementation.createNewSession(input); }, }; }, }, }), ], isInServerlessEnv: true, }; }; ``` ### 3. Create a Supabase client A client is created to interact with Supabase using the `supabase-js` library. #### 3.1 Install the `supabase-js` library ```bash npm install @supabase/supabase-js ``` #### 3.2 Create a new file called `utils/supabase.ts` and add the following: ```ts // utils/supabase.ts let supabase_url = process.env.NEXT_PUBLIC_SUPABASE_URL || "TODO: Your Supabase URL" let supabase_key = process.env.NEXT_PUBLIC_SUPABASE_KEY || "TODO: Your Supabase Key" const getSupabase = (access_token: string) => { const supabase = createClient( supabase_url, supabase_key ) supabase.auth.session = () => ({ access_token, token_type: "", user: null }) return supabase } export { getSupabase } ``` ### 4. Insert users into Supabase when they sign up In this example app, the user can sign up via Email-Password authentication. The API needs to be overridden such that when a user signs up, their email mapped to their userId is stored in Supabase. #### 4.1 Override the Email-Password sign up function ```ts // config/backendConfig.ts let appInfo: AppInfo = { appName: "TODO: add your app name", apiDomain: "TODO: add your website domain", websiteDomain: "TODO: add your website domain" } // take a look at the Creating Supabase Client section to see how to define getSupabase let getSupabase: any; let backendConfig = (): TypeInput => { return { framework: "express", supertokens: { connectionURI: "https://try.supertokens.com", }, appInfo, recipeList: [ ^{recipeNameCapitalLetters}.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, // the signUpPOST function handles sign up signUpPOST: async function (input) { if (originalImplementation.signUpPOST === undefined) { throw Error("Should never come here"); } let response = await originalImplementation.signUpPOST(input); if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); // create a supabase client with the supabase_token from the accessTokenPayload const supabase = getSupabase(accessTokenPayload.supabase_token); // store the user's email mapped to their userId in Supabase const { error } = await supabase .from("users") .insert({ email: response.user.emails[0], user_id: response.user.id }); if (error !== null) { throw error; } } return response; }, }; }, }, }), SessionNode.init({/*...*/}), ], isInServerlessEnv: true, }; }; ``` The Email-Password sign up flow is changed by overriding the `signUpPOST` API. When a user signs up, the `supabase_token` is retrieved from the user's `accessTokenPayload`(this was added in the previous step where the `createNewSession` function was changed) and used to query Supabase to insert the new user's information. ### 5. Retrieve the user email on the frontend With the backend setup, the frontend can be modified to retrieve the user's email from Supabase. ```tsx // pages/index.tsx // take a look at the Creating Supabase Client section to see how to define getSupabase let getSupabase: any; export default function Home() { return ( // The ProtectedPage component is wrapped with the SessionAuth so only an // authenticated user can access it. ) } function ProtectedPage() { // retrieve the authenticated user's accessTokenPayload and userId from the sessionContext const session = useSessionContext() const [userEmail, setEmail] = useState('') useEffect(() => { async function getUserEmail() { if (session.loading) { return; } // retrieve the supabase client who's JWT contains users userId, this is // used by supabase to check that the user can only access table entries which contain their own userId const supabase = getSupabase(session.accessTokenPayload.supabase_token) // retrieve the user's name from the users table whose email matches the email in the JWT const { data } = await supabase.from('users').select('email').eq('user_id', session.userId) if (data.length > 0) { setEmail(data[0].email) } } getUserEmail() }, [session]) if (session.loading) { return null; } return (
SuperTokens 💫

You are authenticated with SuperTokens! (UserId: {session.userId})
Your email retrieved from Supabase: {userEmail}

) } ```
With the backend setup, the frontend can be modified to retrieve the user's email from Supabase. ```tsx // take a look at the Creating Supabase Client section to see how to define getSupabase let getSupabase: any; async function getEmailFromSupabase() { if (await Session.doesSessionExist()) { let accessTokenPayload = await Session.getAccessTokenPayloadSecurely(); const supabase = getSupabase(accessTokenPayload.supabase_token) const { data } = await supabase.from('users').select('email').eq('user_id', await Session.getUserId()) if (data.length > 0) { return data[0].email; } return undefined; } throw new Error("Session does not exist"); } ``` As seen above, the access token payload is fetched from SuperTokens to retrieve the authenticated user's Supabase access token which can be used to fetch the user's email from Supabase. ### 6. Enforce row level security for select and insert requests To enforce Row Level Security for the Users table, you need to create policies for Select and Insert requests. These polices retrieve the `userId` from the JWT and check if it matches the `userId` in the Supabase table. A PostgreSQL function is needed to extract the `userId` from the JWT. The payload in the JWT has the following structure: ```bash { userId, exp } ``` #### 6.1 Create PostgreSQL function to retrieve `userId` from JWT To create the PostgreSQL function, navigate back to the Supabase dashboard, select `SQL` from the sidebar menu, and click `New query`. This creates a new query called `new sql snippet`, which allows you to run any SQL against the Postgres database. Write the following and click `Run`. ```bash create or replace function auth.user_id() returns text as $$ select nullif(current_setting('request.jwt.claims', true)::json->>'userId', '')::text; $$ language sql stable; ``` - This creates a function called `auth.user_id()`, which inspects the `userId` field of our JWT payload. #### 6.2 Create Policies for `SELECT` and `INSERT` queries: ##### `SELECT` query policy The first policy checks whether the user is the owner of the email being retrieved. - Select `Authentication` from the Supabase sidebar menu, click `Policies`, and then `New Policy` on the `Users` table. ![Create policy](/img/thirdpartyemailpassword/supabase/create_policy.png) - From the modal, select `Create a policy from scratch` and add the following. ![select policy](/img/thirdpartyemailpassword/supabase/policy_config_select.png) - This policy is calling the PostgreSQL function we just created to get the currently logged in user's ID `auth.user_id()` and checking whether this matches the `user_id` column for the current `email`. If it does, then it allows the user to select it, otherwise it continues to deny. - Click `Review` and then `Save policy`. ##### `INSERT` query policy The second policy checks whether the `user_id` being inserted is the same as the `userId` in the JWT. - Create another policy and add the following: ![insert policy](/img/thirdpartyemailpassword/supabase/policy_config_insert.png) Similar to the previous policy, the PostgreSQL function that was created is called to get the currently logged in user's ID and check whether this matches the `user_id` column for the row being inserted. If it does, then it allows the user to insert the row, otherwise it continues to deny. Click `Review` and then `Save policy`. ### 6.3 Test your changes You can now sign up and you should see the following screen: ![auth screen](/img/thirdpartyemailpassword/supabase/supabase_app_authenticated_screen.png) If you navigate to your table you should see a new row with the user's `user_id` and `email`. ![table with user](/img/thirdpartyemailpassword/supabase/table_with_user.png) # Authentication - Email Password - Customize form fields Source: https://supertokens.com/docs/authentication/email-password/customize-the-sign-in-form ## Before you start The following instructions are only relevant if you are using the pre-built UI components. If you have created your own authentication UI, you can skip this guide. ## Modify labels and placeholders To change the labels and placeholders of the fields update the `formFields` property, in the recipe configuration. ```tsx preview="/img/emailpassword/signin-with-default-values.png" SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signInForm: { // highlight-start formFields: [{ id: "email", label: "customFieldName", placeholder: "Custom value" }] // highlight-end } } }), Session.init() ] }); ``` ```tsx preview="/img/emailpassword/signin-with-default-values.png" previewAlt="Prebuilt form UI with custom labels and placeholder" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signInForm: { // highlight-start formFields: [{ id: "email", label: "customFieldName", placeholder: "Custom value" }] // highlight-end } } }), supertokensUISession.init() ] }); ``` --- ## Set default values Add a `getDefaultValue` option in the `formFields` configuration to pre-fill the inputs. Keep in mind that the function needs to return a string. ```tsx preview="/img/emailpassword/signin-with-default-values.png" previewAlt="Pre-built sign in form UI with default values for fields" SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signInForm: { formFields: [{ id: "email", label: "Your Email", // highlight-start getDefaultValue: () => "john.doe@gmail.com" // highlight-end }] } } }), Session.init() ] }); ``` ```tsx preview="/img/emailpassword/signin-with-default-values.png" previewAlt="Pre-built sign in form UI with default values for fields" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signInForm: { formFields: [{ id: "email", label: "Your Email", // highlight-start getDefaultValue: () => "john.doe@gmail.com" // highlight-end }] } } }), supertokensUISession.init() ] }); ``` --- ## Change the optional error message When you try to submit the login form without filling in the required fields, the UI, by default, shows an error stating that the `Field is not optional`. To customize this message set the `nonOptionalErrorMsg` property to a custom string. ```tsx preview="/img/emailpassword/signin-with-custom-error-msg.png" previewAlt="Pre-built sign in UI with custom error message" SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signInForm: { formFields: [{ id: "email", label: "Your Email", placeholder: "Email", // highlight-start nonOptionalErrorMsg: "Please add your email" // highlight-end }] } } }), Session.init() ] }); ``` ```tsx preview="/img/emailpassword/signin-with-custom-error-msg.png" previewAlt="Pre-built sign in UI with custom error message" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signInForm: { formFields: [{ id: "email", label: "Your Email", placeholder: "Email", // highlight-start nonOptionalErrorMsg: "Please add your email" // highlight-end }] } } }), supertokensUISession.init() ] }); ``` --- ## Custom field validators To add custom validation logic to the sign in form, update the sign up form configuration. The `email` and `password` fields validation synchronizes between the two forms. Check the [sign up form instructions](/docs/authentication/email-password/customize-the-sign-up-form#change-the-default-email-and-password-validators) for more details. --- # Authentication - Email Password - Customize the sign up form Source: https://supertokens.com/docs/authentication/email-password/customize-the-sign-up-form ## Before you start The next instructions assume that you have a working application that uses **SuperTokens** for authentication. If not, please refer to the [quickstart guide](/docs/quickstart/frontend-setup) and then return here. ## Add extra fields To include more fields in the sign up form, you need to first update both the frontend and backend configuration. Then, when the sing up payload arrives on the backend, you should add a way to persist those values. ### 1. Add the new fields to the UI You first need to add the new fields to your sign up interface. Given that you are using a custom implementation, the steps vary based on your code. After you have updated the form, ensure that the submit action follows the next example. ```tsx async function signUpClicked(email: string, password: string, name: string, age: number, country: string) { let response = await signUp({ formFields: [{ id: "email", value: email }, { id: "password", value: password },{ id: "name", value: name }, { id: "age", value: age + "" }, { id: "country", value: country }] }) // ... rest of the code } ``` ```tsx async function signUpClicked(email: string, password: string, name: string, age: number, country: string) { let response = await supertokensEmailPassword.signUp({ formFields: [{ id: "email", value: email }, { id: "password", value: password }, { id: "name", value: name }, { id: "age", value: age + "" }, { id: "country", value: country }] }) // ... rest of the code } ``` ```bash curl --location --request POST '/signup' --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "formFields": [{ "id": "email", "value": "john@example.com" }, { "id": "password", "value": "somePassword123" }, { id: "name", value: "John Doe" }, { id: "age", value: 27 }, { id: "country", value: "USA" }] }' ``` ```tsx preview="/img/emailpassword/signup-with-name-and-age.png" previewAlt="Prebuilt form UI with extra custom fields" SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { // highlight-start signUpForm: { formFields: [{ id: "name", label: "Full name", placeholder: "First name and last name" }, { id: "age", label: "Your age", placeholder: "How old are you?", }, { id: "country", label: "Your country", placeholder: "Where do you live?", optional: true }] } // highlight-end } }), Session.init() ] }); ``` ```tsx preview="/img/emailpassword/signup-with-name-and-age.png" previewAlt="Prebuilt form UI with extra custom fields" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { // highlight-start signUpForm: { formFields: [{ id: "name", label: "Full name", placeholder: "First name and last name" }, { id: "age", label: "Your age", placeholder: "How old are you?", }, { id: "country", label: "Your country", placeholder: "Where do you live?", optional: true }] } // highlight-end } }), supertokensUISession.init() ] }); ``` #### Create custom components By default, the new fields use `input` elements. To enable more complex fields you can create your own custom components. :::important You may need to disable the Shadow DOM if you're integrating with a different component library that requires you to import its own CSS. For instance, some component libraries, such as [react-international-phone](https://github.com/goveo/react-international-phone), might expect you to include their CSS alongside their components. For more information, refer to [Disable use of shadow DOM](/docs/references/frontend-sdks/prebuilt-ui/shadow-dom). ::: Set the `inputComponent` property for each field that you want to customize. ```tsx preview="/img/emailpassword/signup-with-custom-components.png" previewAlt="Pre-built form UI with custom components" SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { // highlight-start signUpForm: { formFields: [{ id: "select-dropdown", label: "Select Dropdown", inputComponent: ({ value, name, onChange }) => (
), optional: true, }, { id: "terms", label: "", optional: false, nonOptionalErrorMsg: "You must accept the terms and conditions", inputComponent: ({ name, onChange }) => (
onChange(e.target.checked.toString())}> I agree to the{" "} Terms and Conditions
), }] } // highlight-end } }), Session.init() ] }); ```
:::caution This is not applicable for non React apps. You have to create your own custom UI instead. :::
### 2. Include the extra fields in the backend configuration Change the **Backend SDK** initialization call to ensure that the system processes the new fields when a new user registers. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ signUpFeature: { // highlight-start formFields: [{ id: "name" }, { id: "age" }, { id: "country", optional: true }] // highlight-end } }), Session.init({ /* ... */ }) ] }); ``` ```go SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, signUpPOST: async function (input) { if (originalImplementation.signUpPOST === undefined) { throw Error("Should never come here"); } // First we call the original implementation of signUpPOST. let response = await originalImplementation.signUpPOST(input); // Post sign up response, we check if it was successful if (response.status === "OK") { // These are the input form fields values that the user used while signing up let formFields = input.formFields; } return response; } } } } }), Session.init({ /* ... */ }) ] }); ``` ```go } return resp, err } return originalImplementation }, }, }), }, }) } ``` ```python from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword, session from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, SignUpPostOkResult, ) from supertokens_python.recipe.emailpassword.types import FormField from typing import List, Dict, Any, Union from supertokens_python.recipe.session.interfaces import SessionContainer # highlight-start def override_email_password_apis(original_implementation: APIInterface): original_sign_up_post = original_implementation.sign_up_post async def sign_up_post( form_fields: List[FormField], tenant_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], api_options: APIOptions, user_context: Dict[str, Any], ): # First we call the original implementation of sign_up_post. response = await original_sign_up_post( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) # Post sign up response, we check if it was successful if isinstance(response, SignUpPostOkResult): pass # TODO: use the input form fields values for custom logic return response original_implementation.sign_up_post = sign_up_post return original_implementation # highlight-end init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ emailpassword.init( # highlight-start override=emailpassword.InputOverrideConfig( apis=override_email_password_apis ) # highlight-end ), session.init(), ], ) ``` --- ## Customize each form field :::caution Not applicable This section is not relevant for custom UI, as you create your own UI and already have control over the form fields. ::: ### Modify labels and placeholders To change the labels and placeholders of the fields, update the `formFields` property, in the recipe configuration. ```tsx preview="/img/emailpassword/custom-field-name-signup-ep.png" previewAlt="Prebuilt form UI with custom labels and placeholder" SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signUpForm: { // highlight-start formFields: [{ id: "email", label: "customFieldName", placeholder: "Custom value" }] } // highlight-end } }), Session.init() ] }); ``` ```tsx preview="/img/emailpassword/custom-field-name-signup-ep.png" previewAlt="Prebuilt form UI with custom labels and placeholder" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signUpForm: { // highlight-start formFields: [{ id: "email", label: "customFieldName", placeholder: "Custom value" }] } // highlight-end } }), supertokensUISession.init() ] }); ``` ### Set default values Add a `getDefaultValue` option to the `formFields` configuration to set default values. Keep in mind that the function needs to return a string. ```tsx preview="/img/emailpassword/signup-with-default-values.png" previewAlt="Prebuilt form UI with default values for fields" SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [{ id: "email", label: "Your Email", // highlight-start getDefaultValue: () => "john.doe@gmail.com" // highlight-end }, { id: "name", label: "Full name", // highlight-start getDefaultValue: () => "John Doe", // highlight-end }] } } }), Session.init() ] }); ``` ```tsx preview="/img/emailpassword/signup-with-default-values.png" previewAlt="Prebuilt form UI with default values for fields" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [{ id: "email", label: "Your Email", // highlight-start getDefaultValue: () => "john.doe@gmail.com" // highlight-end }, { id: "name", label: "Full name", // highlight-start getDefaultValue: () => "John Doe", // highlight-end }] } } }), supertokensUISession.init() ] }); ``` ### Change the optional error message When you try to submit the login form without filling in the required fields, the UI, by default, shows an error stating that the `Field is not optional`. To customize this message set the `nonOptionalErrorMsg` property to a custom string. ```tsx preview="/img/emailpassword/signup-with-custom-error-msg.png" previewAlt="Prebuilt form UI with custom error message" SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [{ id: "email", label: "Your Email", placeholder: "Email", // highlight-start nonOptionalErrorMsg: "Please add your email" // highlight-end }, { id: "name", label: "Full name", placeholder: "Name", // highlight-start nonOptionalErrorMsg: "Full name is required", // highlight-end }] } } }), Session.init() ] }); ``` ```tsx preview="/img/emailpassword/signup-with-custom-error-msg.png" previewAlt="Prebuilt form UI with custom error message" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [{ id: "email", label: "Your Email", placeholder: "Email", // highlight-start nonOptionalErrorMsg: "Please add your email" // highlight-end }, { id: "name", label: "Full name", placeholder: "Name", // highlight-start nonOptionalErrorMsg: "Full name is required", // highlight-end }] } } }), supertokensUISession.init() ] }); ``` ### Change the field order To customize the order of fields in your sign up form, override the `EmailPasswordSignUpForm` component. Use the next example as a reference. ```tsx preview="/img/emailpassword/signup-with-custom-field-order.png" previewAlt="Pre-built form UI with custom fields order" function App() { return ( { return ( id === 'name')!, props.formFields.find(({ id }) => id === 'email')!, props.formFields.find(({ id }) => id === 'password')!, ]} /> ); }, // highlight-end }}> {/* Rest of the JSX */} ); } export default App; ``` ```tsx preview="/img/emailpassword/signup-with-custom-field-order.png" previewAlt="Pre-built form UI with custom fields order" function App() { if (canHandleRoute([EmailPasswordPreBuiltUI])) { return ( { return ( id === 'name')!, props.formFields.find(({ id }) => id === 'email')!, props.formFields.find(({ id }) => id === 'password')!, ]} /> ); }, // highlight-end }}> {getRoutingComponent([EmailPasswordPreBuiltUI])} ) } return ( {/* Rest of the JSX */} ); } export default App; ``` :::caution This is not applicable for non React apps. You have to create your own custom UI instead. ::: --- ## Change field validators ### 1. Update the frontend configuration :::caution Not applicable For your custom UI, you have to implement field validation checking yourself. Note that you need to also update the backend validation to ensure a complete flow. Check the next section for more details. ::: Add a `validate` method to any of your `formFields`. The following example shows how to add age verification to the form: ```tsx preview="/img/emailpassword/signup-with-name-and-age-failure.png" previewAlt="Pre-built form UI with custom validation" SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [{ id: "name", label: "Full name", placeholder: "First name and last name" }, { id: "age", label: "Your age", placeholder: "How old are you?", optional: true, /* Validation method to make sure that age is above 18 */ // highlight-start validate: async (value) => { if (parseInt(value) > 18) { return undefined; // means that there is no error } return "You must be over 18 to register"; } // highlight-end }, { id: "country", label: "Your country", placeholder: "Where do you live?", optional: true }] } } }), Session.init() ] }); ``` ```tsx preview="/img/emailpassword/signup-with-name-and-age-failure.png" previewAlt="Pre-built form UI with custom validation" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [{ id: "name", label: "Full name", placeholder: "First name and last name" }, { id: "age", label: "Your age", placeholder: "How old are you?", optional: true, /* Validation method to make sure that age is above 18 */ // highlight-start validate: async (value) => { if (parseInt(value) > 18) { return undefined; // means that there is no error } return "You must be over 18 to register"; } // highlight-end }, { id: "country", label: "Your country", placeholder: "Where do you live?", optional: true }] } } }), supertokensUISession.init() ] }); ``` ### 2. Update the backend configuration Add `validate` functions to each of the form fields, in the backend SDK initialization call. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ signUpFeature: { formFields: [{ id: "name" }, { id: "age", /* Validation method to make sure that age >= 18 */ // highlight-start validate: async (value, tenantId) => { if (parseInt(value) >= 18) { return undefined; // means that there is no error } return "You must be over 18 to register"; } // highlight-end }, { id: "country", optional: true }] } }), Session.init({ }) ] }); ``` ```go SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [{ // highlight-start id: "email", label: "...", validate: async (value) => { // Your own validation returning a string or undefined if no errors. return "..."; } }, { id: "password", label: "...", validate: async (value) => { // Your own validation returning a string or undefined if no errors. return "..."; } // highlight-end }] } } }) ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [{ // highlight-start id: "email", label: "...", validate: async (value) => { // Your own validation returning a string or undefined if no errors. return "..."; } }, { id: "password", label: "...", validate: async (value) => { // Your own validation returning a string or undefined if no errors. return "..."; } // highlight-end }] } } }) ] }); ``` ##### 1. Update the backend configuration ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ signUpFeature: { formFields: [{ // highlight-start id: "email", validate: async (value, tenantId) => { // Your own validation returning a string or undefined if no errors. return "..."; } }, { id: "password", validate: async (value, tenantId) => { // Your own validation returning a string or undefined if no errors. return "..."; } // highlight-end }] } }), ] }); ``` ```go SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, // highlight-start termsOfServiceLink: "https://example.com/terms-of-service", privacyPolicyLink: "https://example.com/privacy-policy", // highlight-end recipeList: [/* ... */] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, // highlight-start termsOfServiceLink: "https://example.com/terms-of-service", privacyPolicyLink: "https://example.com/privacy-policy", // highlight-end recipeList: [/* ... */] }); ``` :::caution Not applicable since you do not use the pre-built UI. ::: --- # Authentication - Email Password - Hooks and overrides Source: https://supertokens.com/docs/authentication/email-password/hooks-and-overrides **SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. The following sections describe how you can modify adjust the `emailpassword` recipe to your needs. Explore the [references pages](/docs/references) for a more in depth guide on hooks and overrides. ## Sign in ### Frontend hook This method gets fired, with the `SUCCESS` action, immediately after a successful sign in or sign up. Follow the code snippet to determine if the user is signing up or signing in. With this method you can fire events immediately after a successful sign in. You can use it to send analytics events. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ // highlight-start onHandleEvent: async (context) => { if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } } // highlight-end }), Session.init() ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailPassword.init({ // highlight-start onHandleEvent: async (context) => { if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } } // highlight-end }), supertokensUISession.init() ] }); ``` :::caution Not applicable This section is not applicable for custom UI since you are calling the sign in API yourself anyway. You can perform anything you want to do post sign in based on the result of the API call. ::: ### Backend override Overriding the `signIn` function allows you to introduce your own logic for the sign in process. Use it to persist different types of data or trigger actions. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, signIn: async function (input) { // First we call the original implementation of signIn. let response = await originalImplementation.signIn(input); // Post sign up response, we check if it was successful if (response.status === "OK") { /** * * response.user contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ // TODO: post sign in logic } return response; } } } } // highlight-end }), Session.init({ /* ... */ }) ] }); ``` ```go SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ // highlight-start onHandleEvent: async (context) => { if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } } // highlight-end }), Session.init() ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailPassword.init({ // highlight-start onHandleEvent: async (context) => { if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } } // highlight-end }), supertokensUISession.init() ] }); ``` :::caution Not applicable This section is not applicable for custom UI since you are calling the sign up API yourself anyway. You can perform anything you want to do post sign up based on the result of the API call. ::: ### Backend override Overriding the `signUp` function allows you to introduce your own logic for the sign in process. Use it to persist different types of data, synchronize users between **SuperTokens** and your systems or to trigger other types of actions. ```tsx // backend SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, signUp: async function (input) { // First we call the original implementation of signUp. let response = await originalImplementation.signUp(input); // Post sign up response, we check if it was successful if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { /** * * response.user contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ // TODO: post sign up logic } return response; } } } } // highlight-end }), Session.init({ /* ... */ }) ] }); ``` ```go SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ // highlight-start onHandleEvent: async (context) => { if (context.action === "PASSWORD_RESET_SUCCESSFUL") { // Add you custom logic here } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { // Add you custom logic here } } // highlight-end }), Session.init() ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailPassword.init({ // highlight-start onHandleEvent: async (context) => { if (context.action === "PASSWORD_RESET_SUCCESSFUL") { // Add you custom logic here } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { // Add you custom logic here } } // highlight-end }), supertokensUISession.init() ] }); ``` :::caution Not applicable This section is not applicable for custom UI since you are calling the sign in API yourself anyway. You can perform anything you want to do during the password reset flow based on the result of the API call. ::: ### Backend override Overriding the `passwordResetPOST` function allows you to introduce your own logic for the password reset process. Use it to introduce your own logic for the flow. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ // highlight-start override: { apis: (originalImplementation) => { return { ...originalImplementation, passwordResetPOST: async function(input) { if (originalImplementation.passwordResetPOST === undefined) { throw Error("Should never come here"); } // First we call the original implementation let response = await originalImplementation.passwordResetPOST(input); // Then we check if it was successfully completed if (response.status === "OK") { // TODO: post password reset logic } return response; } }; }, }, // highlight-end }), Session.init() ] }); ``` ```go # Authentication - Email Password - Password hashing Source: https://supertokens.com/docs/authentication/email-password/password-hashing ## Overview **SuperTokens** supports two password hashing algorithms: `BCrypt` and `Argon2`. Per current best practices, `Argon2` is the recommended algorithm. However, **SuperTokens** uses `BCrypt` by default since `Argon2` requires custom settings that are specific to the hardware in which the core is running on. ### Hashing time The key metric to aim for when hashing passwords is the amount of time each hash would take. By default, **SuperTokens** has configured these algorithms to take 300 milliseconds per hash on a machine with 1 GB of `RAM` and 2 virtual `CPU` cores. ## Change the hashing algorithm You can switch algorithms whenever you want. The change affects only the new users that sign up. Previous passwords undergo decryption using the original algorithm. For example, if you hash a password with `BCrypt`, it verifies using `BCrypt` even if the core configuration changes to `Argon2`. Instructions: 1. Go to the SuperTokens SaaS dashboard. 2. Click "Edit Configuration" on the environment you want to change. 3. Find the `password_hashing_alg` property and change it. 4. Click "Save". ```bash docker run \ -p 3567:3567 \ // highlight-start -e PASSWORD_HASHING_ALG=BCRYPT \ -e BCRYPT_LOG_ROUNDS=11 \ // highlight-end -d registry.supertokens.io/supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command password_hashing_alg: BCRYPT bcrypt_log_rounds: 11 ``` ## Hashing calibration This information is relevant only for self hosted core instances. The managed service instances have already calibrated the algorithms based on the hardware. :::important When you change the hashing settings make sure to run the calibration CLI command to find the right balance for your hardware. ::: #### Algorithm settings | Name | Default | Description | | --- | --- | --- | | `password_hashing_alg` | - | This setting chooses which password hashing algorithm to use. For using Argon2, set this to `ARGON2`. | | `argon2_iterations` | `1` | This controls how much `CPU` processing power the hashing process uses. The higher the value, the more processing power, and hence the more time each hash takes. | | `argon2_memory_kb` | `87795` (85 MB) | The amount of memory (`RAM`) that each hash takes. The higher this is, the harder it becomes to crack hashes offline, and the longer the algorithm takes. | | `argon2_parallelism` | `2` | This is the number of threads the algorithm uses during hashing. The higher this is, the harder it would be to crack passwords offline using multiple cores. Should be equal to the number of virtual cores (or twice the number of physical cores) available in the system. | | `argon2_hashing_pool_size` | `1` | This is the maximum number of concurrent hashes that the core performs. A value of `1` means that the core does only one hash at one point in time, other requests for hashing queue up and wait for their turn. | ##### Example If each hash takes 300 milliseconds, a value of `1` here would entail ~ a max of 3 hashes per second (1000 ms / 300 ms). A value of `2` here would entail a max of 6 hashes per second (1000 ms / 300 ms)*2. Password hashing occurs during sign in, sign up, and password reset flows. Therefore, you can set this value according to the target time per hash and how many sign ups/in you expect per second. #### Change the settings ```bash docker run \ -p 3567:3567 \ // highlight-start -e PASSWORD_HASHING_ALG=ARGON2 \ -e ARGON2_ITERATIONS=1 \ -e ARGON2_MEMORY_KB=87795 \ -e ARGON2_PARALLELISM=2 \ -e ARGON2_HASHING_POOL_SIZE=1 \ // highlight-end -d registry.supertokens.io/supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command password_hashing_alg: ARGON2 argon2_iterations: 1 argon2_memory_kb: 87795 argon2_parallelism: 2 argon2_hashing_pool_size: 1 ``` #### Calibrate to your hardware To find the optimal setting for your hardware, you can run the `hashingCalibrate` command via the CLI. This command takes a few parameters: - `--with_alg`: - The value of this should be `argon2` - Compulsory parameter - `--with_time_per_hash_ms`: - This requires the target time per hash (in milliseconds). - The default value is `300`. - `--with_argon2_hashing_pool_size`: - This affects how much memory the hashing process uses per hash. - The default value is `1` - `--with_argon2_max_memory_mb`: - This is the maximum amount of memory (`RAM`) that the core should use for password hashing. The amount of memory per password hash is `with_argon2_max_memory_mb / with_argon2_hashing_pool_size`. - The default value is `1024`. - `--with_argon2_parallelism`: - This is the number of threads that argon2 should use. The higher this is, the harder it becomes to crack passwords offline. - The default value is `2*number of cores` Running the algorithm takes minutes. ```bash docker run registry.supertokens.io/supertokens/supertokens- supertokens hashingCalibrate --with_alg=argon2 ``` ```bash supertokens hashingCalibrate --with_alg=argon2 ``` The above produces an output like: ```text ====Input Settings==== -> Target time per hash (--with_time_per_hash_ms): 300 MS -> Number of max concurrent hashes (--with_argon2_hashing_pool_size): 1 -> Max amount of memory to consume across 1 concurrent hashes (--with_argon2_max_memory_mb): 1024 MB -> Argon2 parallelism (--with_argon2_parallelism): 4 ====Running algorithm==== Current argon2 settings -> memory: 1024 MB -> iterations: 1 Calculating average hashing time.... ..................................................Took 574 MS per hash Adjusting memory to reach target time. Current argon2 settings -> memory: 972 MB -> iterations: 1 Calculating average hashing time.... ..................................................Took 529 MS per hash Adjusting memory to reach target time. Current argon2 settings -> memory: 924 MB -> iterations: 1 Calculating average hashing time.... ..................................................Took 494 MS per hash <....Truncated....> Adjusting memory to reach target time. Current argon2 settings -> memory: 367 MB -> iterations: 2 Calculating average hashing time.... ..................................................Took 319 MS per hash Adjusting memory to reach target time. Current argon2 settings -> memory: 348 MB -> iterations: 2 Calculating average hashing time.... ..................................................Took 303 MS per hash ====Final values==== Average time per hash is: 303 MS argon2_memory_kb: 357104 (348 MB) argon2_iterations: 2 argon2_parallelism: 4 argon2_hashing_pool_size: 1 ==================== You should use these as docker environment variables or put them in the config.yaml file in the SuperTokens installation directory. ``` The contents of the `====Final values====` gives you the values of the parameters to provide to the core. The algorithm starts with the highest amount of memory per hash (= `with_argon2_max_memory_mb/with_argon2_hashing_pool_size`) and `1` iteration. It calculates the current average hashing time by simulating hashes concurrently (based on the value of `with_argon2_hashing_pool_size`). If the hashing time is greater than the target time, it reduces the memory by 5%. If it's less than the target time, it increases the number of iterations. The algorithm stops if the current time is within 10 milliseconds (higher or lower) of the target time. :::info debug If you see an output like: ```bash /usr/bin/supertokens: line 9: 15 Killed "${ST_INSTALL_LOC}"jre/bin/java -classpath "${ST_INSTALL_LOC}cli/*" io.supertokens.cli.Main false "${ST_INSTALL_LOC}" $@ ``` it means that the system doesn't have enough memory. Try to run the algorithm again with a lower memory value by passing `--with_argon2_max_memory_mb` ::: #### Algorithm settings | Name | Default | Description | |------|--------------|-------------| | `password_hashing_alg` | - | This setting chooses which password hashing algorithm to use. For using bcrypt, set this to `BCRYPT`. | | `bcrypt_log_rounds` | `11` | The number of rounds to use for hashing is `2^bcrypt_log_rounds`. The higher this value, the more time hashing takes. | #### Change settings ```bash docker run \ -p 3567:3567 \ // highlight-start -e PASSWORD_HASHING_ALG=BCRYPT \ -e BCRYPT_LOG_ROUNDS=11 \ // highlight-end -d registry.supertokens.io/supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command password_hashing_alg: BCRYPT bcrypt_log_rounds: 11 ``` #### Calibrate to your hardware To find the optimal setting for your hardware, you can run the `hashingCalibrate` command via the CLI. This command takes a few parameters: - `--with_alg`: - The value of this should be `bcrypt`. - Compulsory parameter - `--with_time_per_hash_ms`: - This requires the target time per hash (in milliseconds). - The default value is `300` ```bash docker run registry.supertokens.io/supertokens/supertokens- supertokens hashingCalibrate --with_alg=bcrypt ``` ```bash supertokens hashingCalibrate --with_alg=bcrypt ``` The above produces an output like: ```text ====Input Settings==== -> Target time per hash (--with_time_per_hash_ms): 300 MS ====Running algorithm==== Current log rounds: 11 ..........Took 158 MS per hash Incrementing log rounds and trying again... Current log rounds: 12 ..........Took 310 MS per hash ====Final values==== Average time per hash is: 310 MS bcrypt_log_rounds: 12 ==================== You should use this as a docker environment variable or put this in the config.yaml file in the SuperTokens installation directory. ``` The contents of the `====Final values====` gives you the values of the parameters to provide to the core. The algorithm starts with the minimum recommended value (`11`), and increments it until the average time per hash is greater than the target time. The final value is then equal to the value that yields the closest time per hash as the target one. # Authentication - Email Password - Implement username login Source: https://supertokens.com/docs/authentication/email-password/implement-username-login ## Overview This tutorial shows you how to customize the recipe to add username based login with an optional email field. A few variations exist on how username-based flows can work: | Login Type | Description | Password Reset Flow | |------------|-------------|-------------------| | Username only | User signs up and signs in with username and password | Contact support required | | Username with optional email | User signs up with username and password, email is optional. Can sign in with either username or email | Uses email if provided, otherwise contact support | | Username and email required | User must provide username, email and password during sign up. Can sign in with either username or email | Uses email | This guide implements the second flow: **Username and password login with optional email**. If you are using one of the other options, you can still follow this guide and make tweaks on parts of it to achieve your desired flow. The approach is to update the `email` form field. This way it gets displayed and validated as a username. Then the optional email value gets saved against the `userID` of the user and you use it during sign in and reset password flows. You need to handle the mapping of email to `userID` and store it in your own database. The code snippets below create placeholder functions for you to implement. ## Before you start This guide assumes that you have already implemented the [EmailPassword recipe](/docs/authentication/email-password/introduction) and have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). ## Steps ### 1. Modify the default email validator function Update the backend validator function to check for your username format. The function runs during **sign up**, **sign in**, and **reset password**. Hence it needs to also match an email format since the user might enter it when signing in or resetting their password. Inside **SuperTokens**, the field is still called `email`. This ensures that the username is unique and that the authentication flows works. Use the next code snippet as a reference. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ signUpFeature: { formFields: [{ id: "email", validate: async (value) => { if (typeof value !== "string") { return "Please provide a string input." } // first we check for if it's an email if ( value.match( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ ) !== null ) { return undefined; } // since it's not an email, we check for if it's a correct username if (value.length < 3) { return "Usernames must be at least 3 characters long." } if (!value.match(/^[a-z0-9_-]+$/)) { return "Username must contain only alphanumeric, underscore or hyphen characters." } } }] } }) ] }); ``` ```go if err != nil { msg := "Email is invalid" return &msg } if emailCheck { return nil } // since it's not an email, we check for if it's a correct username if len(value.(string)) < 3 { msg := "Usernames must be at least 3 characters long." return &msg } userNameCheck, err := regexp.Match(`^[a-z0-9_-]+$`, []byte(value.(string))) if err != nil || !userNameCheck { msg := "Username must contain only alphanumeric, underscore or hyphen characters." return &msg } return nil }, }, }, }, }), }, }) } ``` ```python from re import fullmatch from supertokens_python import InputAppInfo, init from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.types import InputFormField from supertokens_python.recipe.emailpassword.utils import InputSignUpFeature async def validate(value: str, tenant_id: str): # first we check for if it's an email if fullmatch( r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', value ) is not None: return None # since it's not an email, we check for if it's a correct username if len(value) < 3: return "Usernames must be at least 3 characters long." if fullmatch(r'^[a-z0-9_-]+$', value) is None: return "Username must contain only alphanumeric, underscore or hyphen characters." return None init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', # type: ignore recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature(form_fields=[ InputFormField(id="email", validate=validate) ]) ) ] ) ``` ### 2. Save the user email #### 2.1 Update the sign up form validation The sign up `API` takes in the username, password, and an optional email. Add a new form field for the email, along with a `validate` function that checks the uniqueness and syntax of the input email. :::warning Custom Implementation To check if the email is unique you need to persist values in your own database and then check against them. ::: ```tsx let emailUserMap: {[key: string]: string} = {} async function getUserUsingEmail(email: string): Promise { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation return emailUserMap[email]; } SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ signUpFeature: { formFields: [{ id: "email", validate: async (value) => { // ...from previous code snippet... return undefined } }, { // highlight-start id: "actualEmail", validate: async (value) => { if (value === "") { // this means that the user did not provide an email return undefined; } if ( value.match( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ ) === null ) { return "Email is invalid"; } if ((await getUserUsingEmail(value)) !== undefined) { return "Email already in use. Please sign in, or use another email" } }, optional: true // highlight-end }] } }) ] }); ``` ```go return nil }, }, // highlight-start { ID: "actualEmail", Validate: func(value interface{}, tenantId string) *string { if value.(string) == "" { // user did not provide an email return nil } // first we check if the input is an email emailCheck, err := regexp.Match(`^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, []byte(value.(string))) if err != nil || !emailCheck { msg := "Email is invalid" return &msg } user, err := getUserUsingEmail(value.(string)) if err != nil || user != nil { msg := "Email already in use. Please sign in, or use another email" return &msg } return nil }, Optional: &actualEmailOptional, }, // highlight-end }, }, }), }, }) } ``` ```python from re import fullmatch from typing import Dict from supertokens_python import InputAppInfo, init from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.types import InputFormField from supertokens_python.recipe.emailpassword.utils import InputSignUpFeature email_user_map: Dict[str, str] = {} # highlight-start async def get_user_using_email(email: str): # TODO: Check your database for if the email is associated with a user # and return that user ID if it is. # this is just a placeholder implementation if email in email_user_map: return email_user_map[email] return None # highlight-end async def validate(value: str, tenant_id: str): # from previous code snippet.. return None async def validate_actual_email(value: str, tenant_id: str): if fullmatch( r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', value ) is None: return "Email is invalid" if (await get_user_using_email(value)) is not None: return "Email already in use. Please sign in, or use another email" init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', # type: ignore recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature(form_fields=[ InputFormField(id="email", validate=validate), # highlight-start InputFormField(id="actualEmail", validate=validate_actual_email, optional=True) # highlight-end ]) ) ] ) ``` #### 2.2 Save the email field value Override the sign up API to save the custom email form field. Use a mapping of `userID` to `email` to keep track of the association. Save the email value in your own database. ```tsx let emailUserMap: {[key: string]: string} = {} async function getUserUsingEmail(email: string): Promise { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation return emailUserMap[email]; } async function saveEmailForUser(email: string, userId: string) { // TODO: Save email and userId mapping // this is just a placeholder implementation emailUserMap[email] = userId } SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ // highlight-start override: { apis: (original) => { return { ...original, signUpPOST: async function (input) { let response = await original.signUpPOST!(input); if (response.status === "OK") { // sign up successful let actualEmail = input.formFields.find(i => i.id === "actualEmail")!.value as string; if (actualEmail === "") { // User did not provide an email. // This is possible since we set optional: true // in the formField config } else { await saveEmailForUser(actualEmail, response.user.id) } } return response } } } }, // highlight-end signUpFeature: { formFields: [ /* ... from previous code snippet ... */] } }) ] }); ``` ```go }, // highlight-start Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { ogSignUpPOST := *originalImplementation.SignUpPOST (*originalImplementation.SignUpPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignUpPOSTResponse, error) { resp, err := ogSignUpPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.SignUpPOSTResponse{}, err } if resp.OK != nil { // sign up successful actualEmail := "" for _, field := range formFields { if field.ID == "email" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.SignUpPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } actualEmail = valueAsString } } if actualEmail == "" { // User did not provide an email. // This is possible since we set optional: true // in the formField config } else { err := saveEmailForUser(actualEmail, resp.OK.User.ID) if err != nil { return epmodels.SignUpPOSTResponse{}, err } } } return resp, nil } return originalImplementation }, }, // highlight-end }), }, }) } ``` ```python from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, SignUpPostOkResult, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.emailpassword.utils import ( InputOverrideConfig, InputSignUpFeature, ) from supertokens_python.recipe.session.interfaces import SessionContainer email_user_map: Dict[str, str] = {} async def get_user_using_email(email: str): # TODO: Check your database for if the email is associated with a user # and return that user ID if it is. # this is just a placeholder implementation if email in email_user_map: return email_user_map[email] return None # highlight-start async def save_email_for_user(email: str, user_id: str): # TODO: Save email and userId mapping # this is just a placeholder implementation email_user_map[email] = user_id def apis_override(original: APIInterface): og_sign_up_post = original.sign_up_post async def sign_up_post( form_fields: List[FormField], tenant_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], api_options: APIOptions, user_context: Dict[str, Any], ): response = await og_sign_up_post( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) if isinstance(response, SignUpPostOkResult): # sign up successful actual_email = "" for field in form_fields: if field.id == "email": actual_email = field.value if actual_email == "": # User did not provide an email. # This is possible since we set optional: true # in the form field config pass else: await save_email_for_user(actual_email, response.user.id) return response original.sign_up_post = sign_up_post return original # highlight-end init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature( form_fields=[ # from previous code snippets... ] ), override=InputOverrideConfig(apis=apis_override), ) ], ) ``` ### 3. Allow username or email during sign in The user should be able to sign in using their email or username along with their password. In the new logic, if a user enters their email, you need to fetch the username associated with that email and then perform the authentication flow. Override the sign in recipe function to allow this. Use the next code snippet as a reference. The example use the `email` to `userId` mapping, mentioned earlier, to figure out which username to use. ```tsx let emailUserMap: { [key: string]: string } = {} async function getUserUsingEmail(email: string): Promise { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation return emailUserMap[email]; } async function saveEmailForUser(email: string, userId: string) { // TODO: Save email and userId mapping // this is just a placeholder implementation emailUserMap[email] = userId } // highlight-start function isInputEmail(input: string): boolean { return input.match( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ ) !== null; } // highlight-end SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { // highlight-start functions: (original) => { return { ...original, signIn: async function (input) { if (isInputEmail(input.email)) { let userId = await getUserUsingEmail(input.email); if (userId !== undefined) { let superTokensUser = await SuperTokens.getUser(userId); if (superTokensUser !== undefined) { // we find the right login method for this user // based on the user ID. let loginMethod = superTokensUser.loginMethods.find(lM => lM.recipeUserId.getAsString() === userId && lM.recipeId === "emailpassword"); if (loginMethod !== undefined) { input.email = loginMethod.email! } } } } return original.signIn(input); } } }, // highlight-end apis: (original) => { return { ...original, // override from previous code snippet } } }, signUpFeature: { formFields: [ /* ... from previous code snippet ... */] } }) ] }); ``` ```go if err != nil || !emailCheck { return false } return true } // highlight-end func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ SignUpFeature: &epmodels.TypeInputSignUp{ FormFields: []epmodels.TypeInputFormField{ /*...from previous code snippet...*/}, }, Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // ...from previous code snippet... return originalImplementation }, // highlight-start Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { ogSignIn := *originalImplementation.SignIn (*originalImplementation.SignIn) = func(email, password, tenantId string, userContext supertokens.UserContext) (epmodels.SignInResponse, error) { if isInputEmail(email) { userId, err := getUserUsingEmail(email) if err != nil { return epmodels.SignInResponse{}, err } if userId != nil { supertokensUser, err := emailpassword.GetUserByID(*userId) if err != nil { return epmodels.SignInResponse{}, err } if supertokensUser != nil { email = supertokensUser.Email } } } return ogSignIn(email, password, tenantId, userContext) } return originalImplementation }, // highlight-end }, }), }, }) } ``` ```python from re import fullmatch from typing import Any, Dict, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import get_user from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import RecipeInterface from supertokens_python.recipe.emailpassword.utils import ( InputOverrideConfig, InputSignUpFeature, ) from supertokens_python.recipe.session.interfaces import SessionContainer email_user_map: Dict[str, str] = {} async def get_user_using_email(email: str): # TODO: Check your database for if the email is associated with a user # and return that user ID if it is. # this is just a placeholder implementation if email in email_user_map: return email_user_map[email] return None async def save_email_for_user(email: str, user_id: str): # TODO: Save email and userId mapping # this is just a placeholder implementation email_user_map[email] = user_id # highlight-start def is_input_email(email: str): return ( fullmatch( r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', email, ) is not None ) def recipe_override(original: RecipeInterface): og_sign_in = original.sign_in async def sign_in( email: str, password: str, tenant_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], user_context: Dict[str, Any], ): if is_input_email(email): user_id = await get_user_using_email(email) if user_id is not None: supertokens_user = await get_user(user_id) if supertokens_user is not None: login_method = next( ( lm for lm in supertokens_user.login_methods if lm.recipe_user_id.get_as_string() == user_id and lm.recipe_id == "emailpassword" ), None, ) if login_method is not None: assert login_method.email is not None email = login_method.email return await og_sign_in( email, password, tenant_id, session, should_try_linking_with_session_user, user_context, ) original.sign_in = sign_in return original # highlight-end init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature( form_fields=[ # from previous code snippets... ] ), override=InputOverrideConfig( # apis=..., from previous code snippet functions=recipe_override ), ) ], ) ``` ### 4. Allow username or email during password reset The password reset flow requires the user to have added an email during sign up. If there is no email associated with the user, return an appropriate message. To update the functionality you have to first change how the password reset token gets generated and then update the email sending logic. This way both methods take into account the new fields. #### 4.1 Override the token generation API The user should enter either their username or their email when starting the password reset flow. Like the sign in customization, you must check if the input is an email and, if it is, retrieve the username associated with the email. If you can't find a username from an email you have to return an appropriate message to the frontend. ```tsx let emailUserMap: { [key: string]: string } = {} async function getUserUsingEmail(email: string): Promise { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation return emailUserMap[email]; } async function saveEmailForUser(email: string, userId: string) { // TODO: Save email and userId mapping // this is just a placeholder implementation emailUserMap[email] = userId } function isInputEmail(input: string): boolean { return input.match( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ ) !== null; } // highlight-start async function getEmailUsingUserId(userId: string) { // TODO: check your database mapping.. // this is just a placeholder implementation let emails = Object.keys(emailUserMap) for (let i = 0; i < emails.length; i++) { if (emailUserMap[emails[i]] === userId) { return emails[i] } } return undefined; } // highlight-end SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { functions: (original) => { return { ...original, // ...override from previous code snippet... } }, apis: (original) => { return { ...original, // ...override from previous code snippet... // highlight-start generatePasswordResetTokenPOST: async function (input) { let emailOrUsername = input.formFields.find(i => i.id === "email")!.value as string; if (isInputEmail(emailOrUsername)) { let userId = await getUserUsingEmail(emailOrUsername); if (userId !== undefined) { let superTokensUser = await SuperTokens.getUser(userId); if (superTokensUser !== undefined) { // we find the right login method for this user // based on the user ID. let loginMethod = superTokensUser.loginMethods.find(lM => lM.recipeUserId.getAsString() === userId && lM.recipeId === "emailpassword"); if (loginMethod !== undefined) { // we replace the input form field's array item // to contain the username instead of the email. input.formFields = input.formFields.filter(i => i.id !== "email") input.formFields = [...input.formFields, { id: "email", value: loginMethod.email! }] } } } } let username = input.formFields.find(i => i.id === "email")!.value as string; let superTokensUsers: supertokensTypes.User[] = await SuperTokens.listUsersByAccountInfo(input.tenantId, { email: username }); // from the list of users that have this email, we now find the one // that has this email with the email password login method. let targetUser = superTokensUsers.find(u => u.loginMethods.find(lM => lM.hasSameEmailAs(username) && lM.recipeId === "emailpassword") !== undefined); if (targetUser !== undefined) { if ((await getEmailUsingUserId(targetUser.id)) === undefined) { return { status: "GENERAL_ERROR", message: "You need to add an email to your account for resetting your password. Please contact support." } } } return original.generatePasswordResetTokenPOST!(input); }, // highlight-end } } }, signUpFeature: { formFields: [ /* ... from previous code snippet ... */] } }) ] }); ``` ```go if err != nil || !emailCheck { return false } return true } // highlight-start func getEmailUsingUserId(userId string) (*string, error) { for email, mappedUserId := range emailUserMap { if mappedUserId == userId { return &email, nil } } return nil, nil } // highlight-end func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ SignUpFeature: &epmodels.TypeInputSignUp{ FormFields: []epmodels.TypeInputFormField{ /*...from previous code snippet...*/ }, }, Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // ...override sign up API from previous code snippet... // highlight-start ogGeneratePasswordResetTokenPOST := *originalImplementation.GeneratePasswordResetTokenPOST (*originalImplementation.GeneratePasswordResetTokenPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.GeneratePasswordResetTokenPOSTResponse, error) { emailOrUsername := "" for _, field := range formFields { if field.ID == "email" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } emailOrUsername = valueAsString } } if isInputEmail(emailOrUsername) { userId, err := getUserUsingEmail(emailOrUsername) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if userId != nil { supertokensUser, err := emailpassword.GetUserByID(*userId) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if supertokensUser != nil { // we replace the input form field's array item // to contain the username instead of the email. newFormFields := []epmodels.TypeFormField{} for _, field := range formFields { if field.ID == "email" { newFormFields = append(newFormFields, epmodels.TypeFormField{ ID: "email", Value: supertokensUser.Email, }) } else { newFormFields = append(newFormFields, field) } } formFields = newFormFields } } } username := "" for _, field := range formFields { if field.ID == "email" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } username = valueAsString } } supertokensUser, err := emailpassword.GetUserByEmail(tenantId, username) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if supertokensUser != nil { email, err := getEmailUsingUserId(supertokensUser.ID) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if email == nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "You need to add an email to your account for resetting your password. Please contact support.", }, }, nil } } return ogGeneratePasswordResetTokenPOST(formFields, tenantId, options, userContext) } // highlight-end return originalImplementation }, Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { // ...override from previous code snippet... return originalImplementation }, }, }), }, }) } ``` ```python from re import fullmatch from typing import Any, Dict, List from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import get_user, list_users_by_account_info from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.emailpassword.utils import ( InputOverrideConfig, InputSignUpFeature, ) from supertokens_python.types import GeneralErrorResponse from supertokens_python.types.base import AccountInfoInput email_user_map: Dict[str, str] = {} async def get_user_using_email(email: str): # TODO: Check your database for if the email is associated with a user # and return that user ID if it is. # this is just a placeholder implementation if email in email_user_map: return email_user_map[email] return None async def save_email_for_user(email: str, user_id: str): # TODO: Save email and userId mapping # this is just a placeholder implementation email_user_map[email] = user_id def is_input_email(email: str): return ( fullmatch( r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', email, ) is not None ) # highlight-start async def get_email_using_user_id(user_id: str): for email in email_user_map: if email_user_map[email] == user_id: return email return None def apis_override(original: APIInterface): og_generate_password_reset_token_post = original.generate_password_reset_token_post async def generate_password_reset_token_post( form_fields: List[FormField], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): email_or_username = "" for field in form_fields: if field.id == "email": email_or_username = field.value if is_input_email(email_or_username): user_id = await get_user_using_email(email_or_username) if user_id is not None: supertokens_user = await get_user(user_id) if supertokens_user is not None: # we find the right login method for this user # based on the user ID. login_method = next( ( lm for lm in supertokens_user.login_methods if lm.recipe_user_id == user_id and lm.recipe_id == "emailpassword" ), None, ) if login_method is not None: # we replace the input form field's array item # to contain the username instead of the email. form_fields = [ field for field in form_fields if field.id != "email" ] form_fields.append( FormField(id="email", value=login_method.email) ) username = "" for field in form_fields: if field.id == "email": username = field.value supertokens_user = await list_users_by_account_info( tenant_id, AccountInfoInput(email=username) ) target_user = next( ( u for u in supertokens_user if any( lm.email == username and lm.recipe_id == "emailpassword" for lm in u.login_methods ) ), None, ) if target_user is not None: if (await get_email_using_user_id(target_user.id)) is None: return GeneralErrorResponse( "You need to add an email to your account for resetting your password. Please contact support." ) return await og_generate_password_reset_token_post( form_fields, tenant_id, api_options, user_context ) original.generate_password_reset_token_post = generate_password_reset_token_post return original # highlight-end init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature( form_fields=[ # from previous code snippets... ] ), override=InputOverrideConfig( # functions=..., from previous code snippet apis=apis_override ), ) ], ) ``` #### 4.2 Override the email sending API Update the email sending API to retrieve the user email if the user used a username in the password reset flow. ```tsx let emailUserMap: {[key: string]: string} = {} async function getUserUsingEmail(email: string): Promise { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation return emailUserMap[email]; } async function saveEmailForUser(email: string, userId: string) { // TODO: Save email and userId mapping // this is just a placeholder implementation emailUserMap[email] = userId } function isInputEmail(input: string): boolean { return input.match( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ ) !== null; } async function getEmailUsingUserId(userId: string) { // TODO: check your database mapping.. // this is just a placeholder implementation let emails = Object.keys(emailUserMap) for (let i = 0; i < emails.length; i++) { if (emailUserMap[emails[i]] === userId) { return emails[i] } } return undefined; } SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { /* ...from previous code snippets... */ }, signUpFeature: { formFields: [ /* ... from previous code snippet ... */] }, // highlight-start emailDelivery: { override: (original) => { return { ...original, sendEmail: async function (input) { input.user.email = (await getEmailUsingUserId(input.user.id))!; return original.sendEmail(input) } } } }, // highlight-end }) ] }); ``` ```go if err != nil || !emailCheck { return false } return true } func getEmailUsingUserId(userId string) (*string, error) { for email, mappedUserId := range emailUserMap { if mappedUserId == userId { return &email, nil } } return nil, nil } func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ SignUpFeature: &epmodels.TypeInputSignUp{ FormFields: []epmodels.TypeInputFormField{ /*...from previous code snippet...*/ }, }, Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // ...override from previous code snippet... return originalImplementation }, Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { // ...override from previous code snippet... return originalImplementation }, }, // highlight-start EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { ogSendEmail := *originalImplementation.SendEmail (*originalImplementation.SendEmail) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) error { email, err := getEmailUsingUserId(input.PasswordReset.User.ID) if err != nil { return err } input.PasswordReset.User.Email = *email return ogSendEmail(input, userContext) } return originalImplementation }, }, // highlight-end }), }, }) } ``` ```python from re import fullmatch from typing import Any, Dict from supertokens_python import InputAppInfo, init from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.types import ( EmailDeliveryOverrideInput, EmailTemplateVars, ) from supertokens_python.recipe.emailpassword.utils import ( InputOverrideConfig, InputSignUpFeature, ) email_user_map: Dict[str, str] = {} async def get_user_using_email(email: str): # TODO: Check your database for if the email is associated with a user # and return that user ID if it is. # this is just a placeholder implementation if email in email_user_map: return email_user_map[email] return None async def save_email_for_user(email: str, user_id: str): # TODO: Save email and userId mapping # this is just a placeholder implementation email_user_map[email] = user_id def is_input_email(email: str): return fullmatch( r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', email ) is not None async def get_email_using_user_id(user_id: str): for email in email_user_map: if email_user_map[email] == user_id: return email return None # highlight-start def custom_email_deliver(original_implementation: EmailDeliveryOverrideInput) -> EmailDeliveryOverrideInput: original_send_email = original_implementation.send_email async def send_email(template_vars: EmailTemplateVars, user_context: Dict[str, Any]) -> None: actual_email = await get_email_using_user_id(template_vars.user.id) if actual_email is None: raise Exception("Should never come here") template_vars.user.email = actual_email return await original_send_email(template_vars, user_context) original_implementation.send_email = send_email return original_implementation # highlight-end init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', # type: ignore recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature(form_fields=[ # from previous code snippets... ]), override=InputOverrideConfig( # functions=..., from previous code snippet # apis=..., from previous code snippet ), # highlight-next-line email_delivery=EmailDeliveryConfig(override=custom_email_deliver) ) ] ) ``` ### 5. Show the new fields in the user interface :::info The following instructions are only relevant if you are using the pre-built UI. If you created your own custom UI on the frontend, please make sure to pass the new email `formField` when you call the sign up function. Even if the user has not given an email, you must add it with an empty string. ::: Update the pre-built UI to reflect the new flow: - Skip frontend validation for the `email` field since username or email is permissible. The backend performs those checks. - Change the "Email" label in the sign up form to say "Username". - Add an extra field in the sign up form where the user can enter their email. - Change the "Email" label in the sign in form to say "Username or email". - Change the "Email" label in the password reset form to say "Username or email". - Update translations for the email field if necessary. ```tsx SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, // highlight-start languageTranslations: { translations: { "en": { EMAIL_PASSWORD_EMAIL_LABEL: "Username or email" } } }, // highlight-end recipeList: [ EmailPassword.init({ signInAndUpFeature: { // highlight-start signInForm: { formFields: [{ id: "email", label: "Username or email", placeholder: "Username or email" }] }, signUpForm: { formFields: [{ id: "email", label: "Username", placeholder: "Username", validate: async (input) => { // the backend validates this anyway. So nothing required here return undefined; } }, { id: "actualEmail", validate: async (input) => { // the backend validates this anyway. So nothing required here return undefined }, label: "Email", optional: true }] } // highlight-end } }), // other recipes initialisation.. ], }); ``` # Authentication - Email Password - Password reset Source: https://supertokens.com/docs/authentication/email-password/password-reset ## Overview The password reset feature consists of two actions: one in which a user requests a reset password link over email and another where the user sets the new password. ### The password reset forms The following images show how the password reset forms render when you are using the pre-built UI. You see this if you navigate to `/reset-password`. UI to send password reset email You see this if you navigate to `/auth/reset-password?token=TOKEN`. UI to change password To implement your own interface create two different forms: - One where the user requests a password reset link. - Another one where the user changes their password. Use the pre-built UI components as a reference. ### The password reset email This is how the email that gets delivered to the learner looks like: Email UI for password reset email You can find the [source code of this template on GitHub](https://github.com/supertokens/email-sms-templates/blob/master/email-html/password-reset.html). To customize the template check the [email delivery](/docs/platform-configuration/email-delivery) section for more information. --- ## Embed the reset form in a page To embed the reset form in a page you can use the next steps. ### 1. Disable the default implementation ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ // highlight-start resetPasswordUsingTokenFeature: { disableDefaultUI: true }, // highlight-end }), ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailPassword.init({ // highlight-start resetPasswordUsingTokenFeature: { disableDefaultUI: true }, // highlight-end }), ] }); ``` If you navigate to `/auth/reset-password`, you should not see the widget anymore. ### 2. Render the component yourself Add the `ResetPasswordUsingToken` component in your app: ```tsx class ResetPasswordPage extends React.Component { render() { return (
// highlight-next-line
) } } ```
:::caution You have to build your own UI for this. :::
:::caution Not applicable since you do not use pre-built UI. ::: ### 3. Change the website path for reset password UI This step is optional. The default path for this is component is `/reset-password`. If you are displaying this at some custom path, then you need to add additional configuration on the backend and frontend: #### 3.1 On the backend ```tsx SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ // highlight-start emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail: async function (input) { if (input.type === "PASSWORD_RESET") { return originalImplementation.sendEmail({ ...input, passwordResetLink: input.passwordResetLink.replace( // This is: `/reset-password` "http://localhost:3000/auth/reset-password", "http://localhost:3000/your/path" ) }) } return originalImplementation.sendEmail(input); } } } } // highlight-end }) ] }); ``` ```go SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ //highlight-start // The user will be taken to the custom path when they click on forgot password. getRedirectionURL: async (context) => { if (context.action === "RESET_PASSWORD") { return "/custom-reset-password-path"; }; } //highlight-end }) ] }) ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ //highlight-start // The user will be taken to the custom path when they click on forgot password. getRedirectionURL: async (context) => { if (context.action === "RESET_PASSWORD") { return "/custom-reset-password-path"; }; } //highlight-end }) ] }) ``` :::caution Not applicable since you do not use pre-built UI. ::: ## Generate a reset link manually You can use the backend SDK to generate the reset password link as shown below: ```tsx async function createResetPasswordLink(userId: string, email: string) { const linkResponse = await EmailPassword.createResetPasswordLink("public", userId, email); if (linkResponse.status === "OK") { console.log(linkResponse.link); } else { // user does not exist or is not an email password user } } ``` ```go ::: --- ## Change the reset's link lifetime By default, the password reset link's lifetime is 1 hour. You can change this via a core's configuration (time in milliseconds): ```bash # Here we set the lifetime to 2 hours. docker run \ -p 3567:3567 \ // highlight-next-line -e EMAIL_VERIFICATION_TOKEN_LIFETIME=7200000 \ -d registry.supertokens.io/supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command email_verification_token_lifetime: 7200000 ``` :::info - For managed service, you can update these values by visiting the dashboard. - This requires that your SuperTokens core version >= `3.6.0` ::: --- # Authentication - Email Password - Disable the Sign Up Flow Source: https://supertokens.com/docs/authentication/email-password/disable-signup Learn how to disable the sign up flow for the `EmailPassword` recipe. ## Overview In order to prevent users from signing up directly through the frontend, you can disable the sign up flow. This can be done in two steps: - Update the **UI** to get rid of any sign up information - Change the **Backend SDK** to prevent sign up attempts ## Before you start This guide assumes that you already have configured your application to use **SuperTokens** for authentication. If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). ## Remove the sign up UI Remove the sign up UI by overriding the `AuthPageComponentList` component and setting the `showSuperTokensAuth` prop to `false`. ```tsx function App() { return ( { return ; }, }}> ); } export default App; ``` Remove the sign up UI by customizing the `CSS` of the authentication page. ```tsx // this goes in the auth route config of your frontend app (once the pre built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, style: ` [data-supertokens~=authPage] [data-supertokens~=headerSubtitle] { display: none; } `, recipeList: [ /* ... */] }); ``` If you have a custom UI, this step will depend on your implementation. Just make sure that the user will not be able to view any sign up elements on the authentication page. ## Disable the Backend SDK sign up endpoints Override the **Backend SDK** API functions to prevent sign up attempts. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, // highlight-next-line signUpPOST: undefined, } } } }) ] }); ``` ```go # Authentication - Email Password - Password managers Source: https://supertokens.com/docs/authentication/email-password/password-managers ## Overview Styling encapsulation relies on the ["shadow DOM" browser feature](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM). Password managers such as Dashlane, LastPass, or OnePassword do not detect authentication forms fields inside shadow DOMs. Therefore, if you would like to make sure that your end users can use their password managers, you have to disable shadow DOM. :::info no-title These instructions are only relevant if you are using the pre-built UI components. ::: ```tsx preview="/img/emailpassword/password-manager.png" previewAlt="Demo of a password manager working with prebuilt UI when shadow DOM is not active" SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, useShadowDom: false, recipeList: [ /* ... */] }); ``` ```tsx preview="/img/emailpassword/password-manager.png" previewAlt="Demo of a password manager working with prebuilt UI when shadow DOM is not active" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, useShadowDom: false, recipeList: [ /* ... */] }); ``` :::caution - SuperTokens uses a special attribute to define its styling. Disabling shadow DOM should not impact the rest of your application's styles. Verify that your CSS does not impact how SuperTokens UI appears when disabling Shadow DOM. - Shadow DOM is always disabled with Internet Explorer since it does not support it. Similarly, if you intend to support Internet Explorer for your application make sure to verify how SuperTokens UI appears. ::: # Authentication - Passwordless - Initial setup Source: https://supertokens.com/docs/authentication/passwordless/initial-setup ## Overview This page shows you how to add the **Passwordless** `recipe` to your project. The tutorial creates a login flow, rendered by either the **Prebuilt UI** components or by your own **Custom UI**. ## Before you start :::important These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). If you have skipped that page please follow the tutorial and return here once you're done. ::: ### Terminology Before going into the actual steps lets first talk about two terms that influence how you configure the **Passwordless** recipe. - **Contact Method**: This defines how the user receives the credentials from your app. You can choose between `email`, `phone number` or both (the user has to choose one during the login flow). - **Flow Type**: This is the actual credential type used for authentication. You can choose between **Magic Link**, **OTP** (One-Time Password), or both (the user has to choose one during the login flow). ## Steps ### 1. Initialize the frontend SDK #### 1.1 Add the `Passwordless` recipe in your main configuration file. ```tsx // highlight-start // highlight-end SuperTokens.init({ appInfo: { // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ // highlight-start Passwordless.init({ contactMethod: "^{recipes.passwordless.contactMethod}" }), // highlight-end Session.init() ] }); ``` #### 1.2 Include the pre-built UI components in your application. To render the **Pre-Built UI** inside your application, you need to specify which routes show the authentication components. The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. ```tsx // highlight-next-line class App extends React.Component { render() { return ( {/*This renders the login UI on the route*/} // highlight-next-line {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [PasswordlessPreBuiltUI])} {/*Your app routes*/} ); } } ``` :::important If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample. Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details. ```tsx function AppRoutes() { const authRoutes = getSuperTokensRoutesForReactRouterDom( reactRouterDom, [/* Add your UI recipes here e.g. EmailPasswordPrebuiltUI, PasswordlessPrebuiltUI, ThirdPartyPrebuiltUI */] ); const routes = useRoutes([ ...authRoutes.map(route => route.props), // Include the rest of your app routes ]); return routes; } function App() { return ( ); } ``` ::: ```tsx class App extends React.Component { render() { // highlight-start if (canHandleRoute([PasswordlessPreBuiltUI])) { // This renders the login UI on the route return getRoutingComponent([PasswordlessPreBuiltUI]) } // highlight-end return ( {/*Your app*/} ); } } ``` Add the `Passwordless` recipe in your `AuthComponent`. ```tsx title="/app/auth/auth.component.ts" @Component({ selector: "app-auth", template: '
', }) export class AuthComponent implements OnDestroy, AfterViewInit { constructor( private renderer: Renderer2, @Inject(DOCUMENT) private document: Document ) { } ngAfterViewInit() { this.loadScript('^{prebuiltUIVersion}'); } ngOnDestroy() { // Remove the script when the component is destroyed const script = this.document.getElementById('supertokens-script'); if (script) { script.remove(); } } private loadScript(src: string) { const script = this.renderer.createElement('script'); script.type = 'text/javascript'; script.src = src; script.id = 'supertokens-script'; script.onload = () => { supertokensUIInit({ appInfo: { // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ // highlight-start supertokensUIPasswordless.init({ contactMethod: "^{recipes.passwordless.contactMethod}" }), // highlight-end supertokensUISession.init(), ], }); } this.renderer.appendChild(this.document.body, script); } } ```
Add the `Passwordless` recipe in your `AuthView` file. ```tsx ```
### 2. Initialize the backend SDK You need to initialize the **Backend SDK** alongside the code that starts your server. The init call includes [configuration details](/docs/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. For the **Passwordless** recipe, you also need to specify the `flowType` and `contactMethod`. Click one of the options from the next form and the code snippet updates. ```tsx title="Backend SDK Init" showAppTypeSelect // highlight-next-line supertokens.init({ // Replace this with the framework you are using framework: "express", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ // highlight-start Passwordless.init({ flowType: "^{recipes.passwordless.flowType}", contactMethod: "^{recipes.passwordless.contactMethod}" }), // highlight-end Session.init() ] }); ``` ```python title="Backend SDK Init" showAppTypeSelect from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import passwordless, session # highlight-next-line ^{derived.pythonContactMethodImport} init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="", website_base_path="" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io^{derived.appIdPathname}", # api_key: ), framework='fastapi', recipe_list=[ session.init(), # initializes session features passwordless.init( flow_type="^{recipes.passwordless.flowType}", contact_config=^{derived.pythonContactMethodMethod}() ) ], mode='asgi' # use wsgi if you are running using gunicorn ) ``` ```go title="Backend SDK Init" showAppTypeSelect
### 1. Initialize the frontend SDK Call the SDK init function at the start of your application. The invocation includes the [main configuration details](/docs/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. ```tsx SuperTokens.init({ appInfo: { apiDomain: "", apiBasePath: "", appName: "...", }, recipeList: [ Session.init(), Passwordless.init(), ], }); ``` First, you need to add the recipe script tag. ```html ``` You can initialize the SDK ```tsx supertokens.init({ appInfo: { apiDomain: "", apiBasePath: "", appName: "...", }, recipeList: [ supertokensSession.init(), supertokensPasswordless.init(), ], }); ``` ```tsx SuperTokens.init({ apiDomain: "", apiBasePath: "", }); ``` Add the `SuperTokens.init` function call at the start of your application. ```kotlin void main() { SuperTokens.init( apiDomain: "", apiBasePath: "", ); } ``` ### 2. Add the login UI The following section shows you what aspects you need to cover to implement the UI for a `Magic Link` flow. The same flow applies during either sign up or sign in. This guide shows you how to determine if the system creates a new user in the next steps. #### 2.1 Sending the Magic link You need to add a form that asks the user for their email address or phone number. When the user submits the form, you need to call the following API to create and send them a **Magic Link**. :::info You configure the contact method on the next page, where you discuss the process of adding the `SDK` to your backend app. ::: ```tsx async function sendMagicLink(email: string) { try { let response = await createCode({ email }); /** * For phone number, use this: let response = await createCode({ phoneNumber: "+1234567890" }); */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason) } else { // Magic link sent successfully. window.alert("Please check your email for the magic link"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, // or if the input email / phone number is not valid. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function sendMagicLink(email: string) { try { let response = await supertokensPasswordless.createCode({ email }); /** * For phone number, use this: let response = await supertokens^{recipeNameCapitalLetters}.createCode({ phoneNumber: "+1234567890" }); */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason) } else { // Magic link sent successfully. window.alert("Please check your email for the magic link"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, // or if the input email / phone number is not valid. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` For email based login ```bash curl --location --request POST '/signinup/code' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johndoe@gmail.com" }' ``` For phone number based login ```bash curl --location --request POST '/signinup/code' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "phoneNumber": "+1234567890" }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the magic link was successfully sent. - `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend, or if the input email or password failed the backend validation logic. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during multi-factor authentication (MFA). The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. The response from the API call is the following object (in case of `status: "OK"`): ```json { status: "OK"; deviceId: string; preAuthSessionId: string; flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; fetchResponse: Response; // raw fetch response from the API call } ``` You want to save the `deviceId` and `preAuthSessionId` on the frontend storage. These are useful to: - Resend a new magic link. - Detect if the user has already sent a magic link before or if this is an entirely new login attempt. This distinction can be important if you have different UI for these two states. For example, if this info already exists, you do not want to show the user an input box to enter their email / phone, and instead want to show them the resend link button. #### 2.2 Resending a magic link After sending the initial magic link to the user, you may want to display a resend button to them. When the user clicks on this button, you should call the following API ```tsx async function resendMagicLink() { try { let response = await resendCode(); if (response.status === "RESTART_FLOW_ERROR") { // this can happen if the user has already successfully logged in into // another device whilst also trying to login to this one. // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth") } else { // Magic link resent successfully. window.alert("Please check your email for the magic link"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function resendMagicLink() { try { let response = await supertokensPasswordless.resendCode(); if (response.status === "RESTART_FLOW_ERROR") { // this can happen if the user has already successfully logged in into // another device whilst also trying to login to this one. // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await supertokensPasswordless.clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth") } else { // Magic link resent successfully. window.alert("Please check your email for the magic link"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash curl --location --request POST '/signinup/code/resend' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "deviceId": "...", "preAuthSessionId": "...." }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the magic link was successfully sent. - `status: "RESTART_FLOW_ERROR"`: This can happen if the user has already successfully logged in into another device whilst also trying to login to this one. You want to take the user back to the login screen where they can enter their email / phone number again. Be sure to remove the stored `deviceId` and `preAuthSessionId` from the frontend storage. - `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. #### How to detect if the user is on step 2.1 or step 2.2? If you are building the UI for both of the previous steps on the same page you might run into an issue when the user refreshes the page. To prevent this you need a way to know which UI to show. ```tsx async function hasInitialMagicLinkBeenSent() { return await getLoginAttemptInfo() !== undefined; } ``` ```tsx async function hasInitialMagicLinkBeenSent() { return await supertokensPasswordless.getLoginAttemptInfo() !== undefined; } ``` If `hasInitialMagicLinkBeenSent` returns `true`, it means that the user has already sent the initial magic link to themselves, and you can show the resend link UI. Else show a form asking them to enter their email / phone number. Since you save the `preAuthSessionId` and `deviceId` after sending the initial magic link, you can know if the user is on either **step 3.1** or **step 3.2**. Check if these tokens are on the device. If they aren't, you should follow **step 3.1**, else follow **step 3.2**. :::important You need to clear these tokens if: - the user navigates away from the **step 3.2** page - you get a `RESTART_FLOW_ERROR` at any point in time from an API call - the user has successfully logged in. ::: #### 2.3 Consuming the magic link When a user clicks on a magic link, you first need to know if the action came from the same browser/device as the one that started the flow. To do this you ca use this code sample. ```tsx async function isThisSameBrowserAndDevice() { return await getLoginAttemptInfo() !== undefined; } ``` ```tsx async function isThisSameBrowserAndDevice() { return await supertokensPasswordless.getLoginAttemptInfo() !== undefined; } ``` Since you save the `preAuthSessionId` and `deviceId`, you can check if they exist on the app. If they do, then it's the same device that the user has opened the link on, else it's a different device. :::important Add a intermediate step if the user came from a different device. ::: If the user clicked on a link from a different device, you need to show some kind of an intermediate UI. This is to protect against email clients opening the magic link on their servers and consuming the link. The page should require additional user interaction before consuming the magic link. For example, you could show a button with the following text: `Click here to login into this device`. On click, you can consume the magic link to log the user into that device. With this understanding of how to avoid potential errors, proceed with the actual instructions on how to authenticate with the magic link. ```tsx async function handleMagicLinkClicked() { try { let response = await consumeCode(); if (response.status === "OK") { // we clear the login attempt info that was added when the createCode function // was called since the login was successful. await clearLoginAttemptInfo(); if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success } window.location.assign("/home") } else { // this can happen if the magic link has expired or is invalid // or if it was denied due to security reasons in case of automatic account linking // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth") } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function handleMagicLinkClicked() { try { let response = await supertokensPasswordless.consumeCode(); if (response.status === "OK") { // we clear the login attempt info that was added when the createCode function // was called since the login was successful. await supertokensPasswordless.clearLoginAttemptInfo(); if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success } window.location.assign("/home") } else { // this can happen if the magic link has expired or is invalid // or if it was denied due to security reasons in case of automatic account linking // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await supertokensPasswordless.clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth") } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` You need to remove the `linkCode` and `preAuthSessionId` from the Magic link. For example, if the Magic link is ```text https://example.com/auth/verify?preAuthSessionId=PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=#s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs= ``` Then the `preAuthSessionId` is the value of the query parameter `preAuthSessionId` (`PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=` in the example), and the `linkCode` is the part after the `#` (`s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs=` in the example). We can then use these to call the consume API ```bash curl --location --request POST '/signinup/code/consume' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "linkCode": "s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs=", "preAuthSessionId": "PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=" }' ``` :::info Multi Tenancy For a multi-tenancy setup, the `` value can fetch from the `tenantId` query parameter from the magic link. If it's not there in the link, you can use the value `"public"` (which is the default tenant). ::: The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" | "RESTART_FLOW_ERROR"`: These responses indicate that the Magic link was invalid or expired. - `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during multi-factor authentication (MFA). The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. The following section shows you what aspects you need to cover to implement the UI for a `OTP`, One-Time Password, flow. The same flow applies during either sign up or sign in. This guide shows you how to determine if you create a new user in the next steps. #### 2.1 Creating and sending the OTP You have to add a form that asks the user for their email address or phone number. When the users submit the form you have to call the following API to create and send them an OTP. ```tsx async function sendOTP(email: string) { try { let response = await createCode({ email }); /** * For phone number, use this: let response = await createPasswordlessCode({ phoneNumber: "+1234567890" }); */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason) } else { // OTP sent successfully. window.alert("Please check your email for an OTP"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, // or if the input email / phone number is not valid. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function sendOTP(email: string) { try { let response = await supertokensPasswordless.createCode({ email }); /** * For phone number, use this: let response = await supertokens^{recipeNameCapitalLetters}.createCode({ phoneNumber: "+1234567890" }); */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason) } else { // OTP sent successfully. window.alert("Please check your email for an OTP"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, // or if the input email / phone number is not valid. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` For email based login ```bash curl --location --request POST '/signinup/code' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johndoe@gmail.com" }' ``` For phone number based login ```bash curl --location --request POST '/signinup/code' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "phoneNumber": "+1234567890" }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the OTP was successfully sent. - `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend, or if the input email or password failed the backend validation logic. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. The response from the API call is the following object (in case of `status: "OK"`): ```json { status: "OK"; deviceId: string; preAuthSessionId: string; flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; fetchResponse: Response; // raw fetch response from the API call } ``` You want to save the `deviceId` and `preAuthSessionId` on the frontend storage. These are useful to: - Resend a new OTP. - Detect if the user has already sent an OTP before or if this is an entirely new login attempt. This distinction can be important if you have different UI for these two states. For example, if this info already exists, you do not want to show the user an input box to enter their email / phone, and instead want to show them the enter OTP form with a resend button. - Verify the user's input OTP. #### 2.2 Resending a OTP After you send the OTP to the user, you may want to display a resend button to them. When the user clicks on this button, you should call the following API ```tsx async function resendOTP() { try { let response = await resendCode(); if (response.status === "RESTART_FLOW_ERROR") { // this can happen if the user has already successfully logged in into // another device whilst also trying to login to this one. // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth") } else { // OTP resent successfully. window.alert("Please check your email for the OTP"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function resendOTP() { try { let response = await supertokensPasswordless.resendCode(); if (response.status === "RESTART_FLOW_ERROR") { // this can happen if the user has already successfully logged in into // another device whilst also trying to login to this one. // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await supertokensPasswordless.clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth") } else { // OTP resent successfully. window.alert("Please check your email for the OTP"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash curl --location --request POST '/signinup/code/resend' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "deviceId": "...", "preAuthSessionId": "...." }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the OTP was successfully sent. - `status: "RESTART_FLOW_ERROR"`: This can happen if the user has already successfully logged in into another device whilst also trying to login to this one. You want to take the user back to the login screen where they can enter their email / phone number again. Be sure to remove the stored `deviceId` and `preAuthSessionId` from the frontend storage. - `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. #### How to detect if the user is on step 3.1 or step 3.2? If you are building the UI for both of the previous steps on the same page you might run into an issue when the user refreshes the page. To prevent this you need a way to know which UI to show. ```tsx async function hasInitialOTPBeenSent() { return await getLoginAttemptInfo() !== undefined; } ``` ```tsx async function hasInitialOTPBeenSent() { return await supertokensPasswordless.getLoginAttemptInfo() !== undefined; } ``` If `hasInitialOTPBeenSent` returns `true`, it means that the user has already sent the initial OTP to themselves, and you can show the enter OTP form + resend OTP button 3.2. Else show a form asking them to enter their email / phone number 3.1. Since you save the `preAuthSessionId` and `deviceId` after you send the initial OTP, you can determine if the user is in step 3.1 or 3.2. Check if you stored these tokens on the device. If they aren't, you should follow 3.1, else follow 3.2. :::important You need to clear these tokens if: - the user navigates away from the 3.2 page - you get a `RESTART_FLOW_ERROR` at any point in time from an API call - the user has successfully logged in. ::: #### 2.3 Verifying the OTP When the user enters an OTP you have to call the following API to verify it ```tsx async function handleOTPInput(otp: string) { try { let response = await consumeCode({ userInputCode: otp }); if (response.status === "OK") { // we clear the login attempt info that was added when the createCode function // was called since the login was successful. await clearLoginAttemptInfo(); if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success } window.location.assign("/home") } else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") { // the user entered an invalid OTP window.alert("Wrong OTP! Please try again. Number of attempts left: " + (response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount)); } else if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") { // it can come here if the entered OTP was correct, but has expired because // it was generated too long ago. window.alert("Old OTP entered. Please regenerate a new one and try again"); } else { // this can happen if the user tried an incorrect OTP too many times. // or if it was denied due to security reasons in case of automatic account linking // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth") } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function handleOTPInput(otp: string) { try { let response = await supertokensPasswordless.consumeCode({ userInputCode: otp }); if (response.status === "OK") { // we clear the login attempt info that was added when the createCode function // was called since the login was successful. await supertokensPasswordless.clearLoginAttemptInfo(); if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success } window.location.assign("/home") } else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") { // the user entered an invalid OTP window.alert("Wrong OTP! Please try again. Number of attempts left: " + (response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount)); } else if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") { // it can come here if the entered OTP was correct, but has expired because // it was generated too long ago. window.alert("Old OTP entered. Please regenerate a new one and try again"); } else { // this can happen if the user tried an incorrect OTP too many times. // or if it was denied due to security reasons in case of automatic account linking // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await supertokensPasswordless.clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth") } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash curl --location --request POST '/signinup/code/consume' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "deviceId": "...", "preAuthSessionId": "...", "userInputCode": "" }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "INCORRECT_USER_INPUT_CODE_ERROR"`: The entered OTP is invalid. The response contains information about the maximum number of retries and the number of failed attempts. - `status: "EXPIRED_USER_INPUT_CODE_ERROR"`: The entered OTP is too old. You should ask the user to resend a new OTP and try again. - `status: "RESTART_FLOW_ERROR"`: These responses that the user tried invalid OTPs too many times. - `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. On success, the backend sends back session tokens as part of the response headers, which are automatically handled by the frontend SDK for you. ### 3. Initialize the backend SDK You need to initialize the **Backend SDK** alongside the code that starts your server. The init call includes [configuration details](/docs/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. For the **Passwordless** recipe, you also need to specify the `flowType` and `contactMethod`. Click one of the options from the next form and the code snippet updates. ```tsx title="Backend SDK Init" showAppTypeSelect // highlight-next-line supertokens.init({ // Replace this with the framework you are using framework: "express", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ // highlight-start Passwordless.init({ flowType: "^{recipes.passwordless.flowType}", contactMethod: "^{recipes.passwordless.contactMethod}" }), // highlight-end Session.init() ] }); ``` ```python title="Backend SDK Init" showAppTypeSelect from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import passwordless, session # highlight-next-line ^{derived.pythonContactMethodImport} init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="", website_base_path="" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io^{derived.appIdPathname}", # api_key: ), framework='fastapi', recipe_list=[ session.init(), # initializes session features passwordless.init( flow_type="^{recipes.passwordless.flowType}", contact_config=^{derived.pythonContactMethodMethod}() ) ], mode='asgi' # use wsgi if you are running using gunicorn ) ``` ```go title="Backend SDK Init" showAppTypeSelect Having completed the main setup, you can explore more advanced topics related to the **Passwordless** recipe. Customize the Magic Link Change how Magic Links get created. OTP Customization Change the format of the generated One-Time Password. Hooks and overrides Add custom logic after the logs in or signs up. Email Delivery Customize how emails get delivered to your users. SMS Delivery Customize how SMS messages get delivered to your users. # Authentication - Passwordless - Customize the magic link Source: https://supertokens.com/docs/authentication/passwordless/customize-the-magic-link ## Change the magic link URL ### Override the email delivery backend function You can change the URL of Magic Links by providing overriding the email delivery configuration on the backend. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL", // This example will work with any contactMethod // This example works with the "USER_INPUT_CODE_AND_MAGIC_LINK" and "MAGIC_LINK" flows. flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // highlight-start emailDelivery: { // highlight-start override: (originalImplementation) => { return { ...originalImplementation, sendEmail: async function (input) { return originalImplementation.sendEmail({ ...input, urlWithLinkCode: input.urlWithLinkCode?.replace( // This is: `/verify` "http://localhost:3000/auth/verify", "http://your.domain.com/your/path" ) }) } } } } // highlight-end }), Session.init({ /* ... */ }) ] }); ``` ```go Passwordless.init({ contactMethod: "EMAIL", // This example will work with any contactMethod linkClickedScreenFeature: { disableDefaultUI: true }, }); ``` #### 2. Render the link clicked screen on your custom route: ```tsx function CustomLinkClickedScreen () { return } ``` When the user clicks the magic link, you need to build your own UI on that page to handle the link clicked. You also need to disable the pre-built UI provided by the SDK for the link clicked screen as shown below: ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIPasswordless.init({ contactMethod: "EMAIL", // This example will work with any contactMethod linkClickedScreenFeature: { disableDefaultUI: true }, }); ``` :::info Caution Not applicable since you do not use the pre-built UI ::: --- ## Generate the link manually You can use the backend SDK to generate magic links as shown below: ```tsx async function createMagicLink(email: string) { const magicLink = await Passwordless.createMagicLink({email, tenantId: "public"}); console.log(magicLink); } ``` ```go ::: --- ## Change the link lifetime You can change how long a user can use an OTP or a Magic Link to log in by changing the `passwordless_code_lifetime` core configuration value. You configure this value in milliseconds and it defaults to `900000` (15 minutes). :::caution Each new OTP / magic link generated, either by opening a new browser or by clicking on the "Resend" button, has a lifetime according to the `passwordless_code_lifetime` setting. ::: ```bash docker run \ -p 3567:3567 \ // highlight-start -e PASSWORDLESS_CODE_LIFETIME=60000 \ // highlight-end -d registry.supertokens.io/supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command passwordless_code_lifetime: 60000 ``` - Navigate to your SuperTokens managed service dashboard, and click on the Edit Configuration button. - In there, change the values of the following fields, and click on save. ```yaml passwordless_code_lifetime: 60000 ``` --- # Authentication - Passwordless - Customize the one-time password Source: https://supertokens.com/docs/authentication/passwordless/customize-the-otp ## Change the OTP format By default, the generated OTP is 6 digits long and is numbers only. You can change this to be any length you like and have any character set by providing the `getCustomUserInputCode` function. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL", // This example will work with any contactMethod // This example works with the "USER_INPUT_CODE_AND_MAGIC_LINK" and "USER_INPUT_CODE" flows. flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // highlight-start getCustomUserInputCode: async (userCtx) => { // TODO: return "123abcd"; }, // highlight-end }) ] }); ``` ```go # Authentication - Passwordless - Hooks and overrides Source: https://supertokens.com/docs/authentication/passwordless/hooks-and-overrides **SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. The following sections describe how you can adjust the `passwordless` recipe to your needs. Explore the [references pages](/docs/references) for a more in depth guide on hooks and overrides. ## Frontend hook This method gets fired, after certain events in the `passwordles` authentication flow. Use it to fire different types of events immediately and introduce custom logic based on your use case. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", // highlight-start onHandleEvent: async (context) => { if (context.action === "PASSWORDLESS_RESTART_FLOW") { // TODO: } else if (context.action === "PASSWORDLESS_CODE_SENT") { // TODO: } else { let {id, emails, phoneNumbers} = context.user; if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } } } // highlight-end }), Session.init() ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIPasswordless.init({ contactMethod: "EMAIL_OR_PHONE", // highlight-start onHandleEvent: async (context) => { if (context.action === "PASSWORDLESS_RESTART_FLOW") { // TODO: } else if (context.action === "PASSWORDLESS_CODE_SENT") { // TODO: } else { let {id, emails, phoneNumbers} = context.user; if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } } } // highlight-end }), supertokensUISession.init() ] }); ``` :::caution Not applicable This section is not applicable for custom UI since you are calling the consume code API yourself anyway. You can perform any actions post sign in / up based on the result of the API call. ::: ## Backend override Overriding the `consumeCode` function allows you to introduce your own logic for the authentication process. Use it to persist different types of data or trigger actions. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL", // This example will work with any contactMethod flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // This example will work with any flowType // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, consumeCode: async (input) => { // First we call the original implementation of consumeCode. let response = await originalImplementation.consumeCode(input); // Post sign up response, we check if it was successful if (response.status === "OK") { let { id, emails, phoneNumbers } = response.user; if (input.session === undefined) { if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // TODO: post sign up logic } else { // TODO: post sign in logic } } } return response; } } } } // highlight-end }), Session.init({ /* ... */ }) ] }); ``` ```go # Authentication - Passwordless - Configure email and SMS behavior Source: https://supertokens.com/docs/authentication/passwordless/configure-email-and-sms-behavior ## Changing email / SMS resend time interval :::caution no-title These instructions are only applicable if you are using the pre-built UI. ::: You can set `resendEmailOrSMSGapInSeconds` to establish a minimum delay before the frontend allows the user to click the "Resend" button. This limit is only enforced on the client-side. For API rate-limiting please check out the [deployment section](/docs/deployment/rate-limits). ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", // This example will work with any contactMethod. // highlight-start signInUpFeature: { // The default value is 15 seconds resendEmailOrSMSGapInSeconds: 60, } // highlight-end }), Session.init({ /* ... */ }) ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIPasswordless.init({ contactMethod: "EMAIL_OR_PHONE", // This example will work with any contactMethod. // highlight-start signInUpFeature: { // The default value is 15 seconds resendEmailOrSMSGapInSeconds: 60, } // highlight-end }), supertokensUISession.init({ /* ... */ }) ] }); ``` --- ## Setting default country for phone inputs :::caution no-title These instructions are only applicable if you are using the pre-built UI. ::: Since your delivery method is email, this section is not relevant. If you would still like to see the content, you can resubmit your desired configuration by clicking on the button above. By default, there is no default country selected. This means that users have to select / type in their phone number international code when signing in / signing up. If you would like to set a default country (for all users), then you should use the `defaultCountry` configuration: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Passwordless.init({ contactMethod: "PHONE", // highlight-start signInUpFeature: { /* * Must be a two-letter ISO country code (e.g.: "US") */ defaultCountry: "HU", } // highlight-end }), Session.init({ /* ... */ }) ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIPasswordless.init({ contactMethod: "PHONE", // highlight-start signInUpFeature: { /* * Must be a two-letter ISO country code (e.g.: "US") */ defaultCountry: "HU", } // highlight-end }), supertokensUISession.init({ /* ... */ }) ] }); ``` By default, there is no default country selected. This means that users have to select / type in their phone number international code when signing in / signing up. If you would like to set a default country (for all users), then you should use the `defaultCountry` configuration: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", // highlight-start signInUpFeature: { /* * Must be a two-letter ISO country code (e.g.: "US") */ defaultCountry: "HU", } // highlight-end }), Session.init({ /* ... */ }) ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIPasswordless.init({ contactMethod: "EMAIL_OR_PHONE", // highlight-start signInUpFeature: { /* * Must be a two-letter ISO country code (e.g.: "US") */ defaultCountry: "HU", } // highlight-end }), supertokensUISession.init({ /* ... */ }) ] }); ``` --- # Authentication - Passwordless - Passwordless login via invite link Source: https://supertokens.com/docs/authentication/passwordless/invite-link-flow ## Overview In this flow, the admin of the app calls an API to sign up a user and send them an invite link. Once the user clicks on that, they log in and can access the app. If a user has not received an invitation before, their sign in attempt fails. ## Before you start This guide assumes that you have already implemented the [EmailPassword recipe](/docs/authentication/email-password/introduction) and have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). ## Steps ### 1. Add the ability to invite new users Add a new endpoint that allows you to invite users to your app. You need to first create the new user and then use the `passwordless` API to send the magic link to them. Additionally, protect the endpoint with a role requirement. The `passwordless` API uses the default magic link path, `/auth/verify`, for the invite link. If you are using the pre-built UI, the frontend SDK automatically logs the user in. For custom UI implementations, use the [`consumeCode` function provided by the frontend SDK](/docs/authentication/passwordless/initial-setup) to call the `passwordless` API that verifies the code in the URL and creates the user. ```tsx let app = express(); app.post("/create-user", verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } }), async (req: SessionRequest, res) => { let email = req.body.email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email }) let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email }); // TODO: send inviteLink to user's email res.send("Success"); }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/create-user", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } }) }, ], }, handler: async (req: SessionRequest, res) => { let email = (req.payload.valueOf() as any).email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email }) let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email }); // TODO: send inviteLink to user's email res.response("Success").code(200); } }) ``` ```tsx let fastify = Fastify(); fastify.post("/create-user", { preHandler: verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } }), }, async (req, res) => { let email = req.body.email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email }) let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email }); // TODO: send inviteLink to user's email res.code(200).send("Success"); }); ``` ```tsx async function createUser(awsEvent: SessionEventV2) { let email = JSON.parse(awsEvent.body!).email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email }) let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email }); // TODO: send inviteLink to user's email return { statusCode: '200', body: "Success" } }; exports.handler = verifySession(createUser, { overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } }); ``` ```tsx let router = new KoaRouter(); router.post("/create-user", verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } }), async (ctx: SessionContext, next) => { let email = (ctx.body as any).email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email }) let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email }); // TODO: send inviteLink to user's email ctx.status = 200; ctx.body = "Success"; }); ``` ```tsx class LikeComment { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/create-user") @intercept(verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } })) async handler() { let email = "" // TODO: get from request body // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email }) let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email }); // TODO: send inviteLink to user's email // TODO: send 200 response to the client } } ``` ```tsx export default async function createUser(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } })(req, res, next); }, req, res ) let email = req.body.email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email }) let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email }); // TODO: send inviteLink to user's email res.status(200).json({ message: 'Success' }) } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } const body = await request.json(); let email = body.email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email }) let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email }); // TODO: send inviteLink to user's email return NextResponse.json({ message: 'Success' }); }, { overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } }); } ``` ```tsx // @ts-ignore @Controller() export class CreateUserController { @Post('create-user') @UseGuards(new AuthGuard({ overrideGlobalClaimValidators: async function (globalClaimValidators: any) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } })) // For more information about this guard please read our NestJS guide. async postAPI(@Session() session: SessionContainer): Promise { let email = "" // TODO: get from request body // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email }) let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email }); // TODO: send inviteLink to user's email // TODO: send 200 response to the client } } ``` ```go // This will create the user in supertokens if they don't already exist. tenantId := "public" passwordless.SignInUpByEmail(tenantId, email) inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email) if err != nil { // TODO: send 500 to the client return } fmt.Println(inviteLink) // TODO: send invite link // TODO: send 200 to the client } ``` ```go // This will create the user in supertokens if they don't already exist. tenantId := "public" passwordless.SignInUpByEmail(tenantId, email) inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email) if err != nil { // TODO: send 500 to the client return } fmt.Println(inviteLink) // TODO: send invite link // TODO: send 200 to the client } ``` ```go // This will create the user in supertokens if they don't already exist. tenantId := "public" passwordless.SignInUpByEmail(tenantId, email) inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email) if err != nil { // TODO: send 500 to the client return } fmt.Println(inviteLink) // TODO: send invite link // TODO: send 200 to the client } ``` ```go // This will create the user in supertokens if they don't already exist. tenantId := "public" passwordless.SignInUpByEmail(tenantId, email) inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email) if err != nil { // TODO: send 500 to the client return } fmt.Println(inviteLink) // TODO: send invite link // TODO: send 200 to the client } ``` ```python from fastapi import Depends from supertokens_python.recipe.passwordless.asyncio import create_magic_link, signinup from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.userroles import UserRoleClaim @app.post('/create-user') # type: ignore async def create_user(session: SessionContainer = Depends(verify_session( override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")] ))): email = "" # TODO: read from request body. # this will creat the user in supertokens if they don't already exist await signinup("public", email, None) invite_link = await create_magic_link("public", email, None) print(invite_link) # TODO: send invite_link to email # TODO: send 200 responspe to client ``` ```python from supertokens_python.recipe.passwordless.syncio import create_magic_link, signinup from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.userroles import UserRoleClaim @app.route('/create_user', methods=['POST']) # type: ignore @verify_session( override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")] ) def create_user(): email = "" # TODO: read from request body. # this will creat the user in supertokens if they don't already exist signinup("public", email, None) invite_link = create_magic_link("public", email, None) print(invite_link) # TODO: send invite_link to email # TODO: send 200 responspe to client ``` ```python from django.http import HttpRequest from supertokens_python.recipe.passwordless.asyncio import create_magic_link, signinup from supertokens_python.recipe.session.framework.django.asyncio import verify_session from supertokens_python.recipe.userroles import UserRoleClaim @verify_session( override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")] ) async def create_user(request: HttpRequest): email = "" # TODO: read from request body. # this will creat the user in supertokens if they don't already exist await signinup("public", email, None) invite_link = await create_magic_link("public", email, None) print(invite_link) # TODO: send invite_link to email # TODO: send 200 responspe to client ``` :::info Multi Tenancy In the above code snippets, the `"public"` `tenantId` passes when calling the functions - this is the default `tenantId`. If you are using the multi-tenancy feature, you can pass in a different `tenantId` and this ensures that the user with that email adds only to that tenant. You also need to pass in the `tenantId` to the createMagicLink function which adds the `tenantId` to the generated magic link. The resulting link uses the `websiteDomain` configured in the `appInfo` object in `SuperTokens.init`, but you can change the link's domain to match that of the tenant before sending it. ::: ### 2. Check if a user was invited Update the backend SDK API function to only allow sign up requests from invited users. To do this you need to check if a user exists in **SuperTokens**. ```tsx Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // typecheck-only, removed from output override: { apis: (originalImplementation) => { return { ...originalImplementation, createCodePOST: async function (input) { if ("email" in input) { let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { email: input.email }); let existingPasswordlessUser = existingUsers.find(user => user.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== undefined); if (existingPasswordlessUser === undefined) { // this is sign up attempt return { status: "GENERAL_ERROR", message: "Sign up disabled. Please contact the admin." } } } else { let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { phoneNumber: input.phoneNumber }); let existingPasswordlessUser = existingUsers.find(user => user.loginMethods.find(lM => lM.hasSamePhoneNumberAs(input.phoneNumber) && lM.recipeId === "passwordless") !== undefined); if (existingPasswordlessUser === undefined) { // this is sign up attempt return { status: "GENERAL_ERROR", message: "Sign up disabled. Please contact the admin." } } } return await originalImplementation.createCodePOST!(input); } } } } }) ``` ```go flow_type="USER_INPUT_CODE", override=passwordless.InputOverrideConfig( apis=override_passwordless_apis, ), ) ], ) ``` --- # Authentication - Passwordless - Passwordless login via allow list Source: https://supertokens.com/docs/authentication/passwordless/allow-list-flow ## Overview In this flow, you create a list of emails or phone numbers that are allowed to sign up. Based on that users can go through the passwordless flow. ## Before you start This guide assumes that you already have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). ### Prerequisites This guide uses the `UserMetadata` recipe to store the allow list. You need to [enable it](/docs/post-authentication/user-management/user-metadata) in the SDK initialization step. ## Steps ### 1. Add a way to keep track of allowed emails or phone numbers Start by maintaining an allow list of emails. You can either store this list in your own database, or use the metadata feature provided by SuperTokens to store this. This may seem like a strange use case of the user metadata recipe provided, but it works. The following code samples show you how to save the allow list in the user metadata. ```tsx async function addEmailToAllowlist(email: string) { let existingData = await UserMetadata.getUserMetadata("emailAllowList"); let allowList: string[] = existingData.metadata.allowList || []; allowList = [...allowList, email]; await UserMetadata.updateUserMetadata("emailAllowList", { allowList }); } async function isEmailAllowed(email: string) { let existingData = await UserMetadata.getUserMetadata("emailAllowList"); let allowList: string[] = existingData.metadata.allowList || []; return allowList.includes(email); } async function addPhoneNumberToAllowlist(phoneNumber: string) { let existingData = await UserMetadata.getUserMetadata("phoneNumberAllowList"); let allowList: string[] = existingData.metadata.allowList || []; allowList = [...allowList, phoneNumber]; await UserMetadata.updateUserMetadata("phoneNumberAllowList", { allowList }); } async function isPhoneNumberAllowed(phoneNumber: string) { let existingData = await UserMetadata.getUserMetadata("phoneNumberAllowList"); let allowList: string[] = existingData.metadata.allowList || []; return allowList.includes(phoneNumber); } ``` ```go ::: ### 2. Check if the user is on the allow list Update the backend SDK API function to only allow sign up requests from users that are on the allow list. To do this you need to use the check functions from the previous code snippet. ```tsx declare let isEmailAllowed: (email: string) => Promise // typecheck-only, removed from output declare let isPhoneNumberAllowed: (email: string) => Promise // typecheck-only, removed from output Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // typecheck-only, removed from output override: { apis: (originalImplementation) => { return { ...originalImplementation, createCodePOST: async function (input) { if ("email" in input) { let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { email: input.email, }); let userWithPasswordles = existingUsers.find(u => u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== undefined); if (userWithPasswordles === undefined) { // this is sign up attempt if (!(await isEmailAllowed(input.email))) { return { status: "GENERAL_ERROR", message: "Sign up disabled. Please contact the admin." } } } } else { let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { phoneNumber: input.phoneNumber, }); let userWithPasswordles = existingUsers.find(u => u.loginMethods.find(lM => lM.hasSamePhoneNumberAs(input.phoneNumber) && lM.recipeId === "passwordless") !== undefined); if (userWithPasswordles === undefined) { // this is sign up attempt if (!(await isPhoneNumberAllowed(input.phoneNumber))) { return { status: "GENERAL_ERROR", message: "Sign up disabled. Please contact the admin." } } } } return await originalImplementation.createCodePOST!(input); } } } } }) ``` ```go return false, nil } func isPhoneNumberAllowed(phoneNumber string) (bool, error) { // ... from previous code snippet return false, nil } func main() { passwordless.Init(plessmodels.TypeInput{ Override: &plessmodels.OverrideStruct{ APIs: func(originalImplementation plessmodels.APIInterface) plessmodels.APIInterface { originalCreateCodePOST := *originalImplementation.CreateCodePOST (*originalImplementation.CreateCodePOST) = func(email, phoneNumber *string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.CreateCodePOSTResponse, error) { if email != nil { existingUser, err := passwordless.GetUserByEmail(tenantId, *email) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if existingUser == nil { // sign up attempt emailAllowed, err := isEmailAllowed(*email) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if !emailAllowed { return plessmodels.CreateCodePOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "Sign ups are disabled. Please contact the admin.", }, }, nil } } } else { existingUser, err := passwordless.GetUserByPhoneNumber(tenantId, *phoneNumber) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if existingUser == nil { // sign up attempt phoneNumberAllowed, err := isPhoneNumberAllowed(*phoneNumber) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if !phoneNumberAllowed { return plessmodels.CreateCodePOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "Sign ups are disabled. Please contact the admin.", }, }, nil } } } return originalCreateCodePOST(email, phoneNumber, tenantId, options, userContext) } return originalImplementation }, }, }) } ``` ```python from typing import Any, Dict, Optional, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless.interfaces import ( APIInterface, APIOptions, ) from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.types import GeneralErrorResponse from supertokens_python.types.base import AccountInfoInput async def is_email_allowed(email: str): # from previous code snippet.. return False async def is_phone_number_allowed(phone_number: str): # from previous code snippet.. return False def override_passwordless_apis(original_implementation: APIInterface): original_create_code_post = original_implementation.create_code_post async def create_code_post( email: Union[str, None], phone_number: Union[str, None], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): if email is not None: existing_user = await list_users_by_account_info( tenant_id, AccountInfoInput(email=email) ) user_with_passwordless = next( ( user for user in existing_user if any( login_method.recipe_id == "passwordless" and login_method.has_same_email_as(email) for login_method in user.login_methods ) ), None, ) if user_with_passwordless is None: # sign up attempt if not (await is_email_allowed(email)): return GeneralErrorResponse( "Sign ups disabled. Please contact admin." ) else: assert phone_number is not None existing_user = await list_users_by_account_info( tenant_id, AccountInfoInput(phone_number=phone_number) ) user_with_passwordless = next( ( user for user in existing_user if any( login_method.recipe_id == "passwordless" and login_method.has_same_phone_number_as(phone_number) for login_method in user.login_methods ) ), None, ) if user_with_passwordless is None: # sign up attempt if not (await is_phone_number_allowed(phone_number)): return GeneralErrorResponse( "Sign ups disabled. Please contact admin." ) return await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) original_implementation.create_code_post = create_code_post return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ passwordless.init( contact_config="", # type: ignore # typecheck-only, removed from output flow_type="USER_INPUT_CODE", override=passwordless.InputOverrideConfig( apis=override_passwordless_apis, ), ) ], ) ``` --- # Authentication - Social Login - Initial setup Source: https://supertokens.com/docs/authentication/social/initial-setup ## Overview This page shows you how to authenticate, using **ThirdParty Providers**, with **SuperTokens**. The tutorial creates a login flow, rendered by either the **Prebuilt UI** components or by your own **Custom UI**. ## Before you start :::important These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). If you have skipped that page please follow the tutorial and return here once you're done. ::: ## Steps ### 1. Initialize the frontend SDK #### 1.1 Add the `ThirdParty` recipe in your main configuration file. ```tsx // highlight-next-line SuperTokens.init({ appInfo: { // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ // highlight-start ThirdParty.init({ signInAndUpFeature: { providers: [ Github.init(), Google.init(), Facebook.init(), Apple.init(), ] } }), // highlight-end Session.init() ] }); ``` #### 1.2 Include the pre-built UI components in your application. In order for the **pre-built UI** to render inside your application, you have to specify which routes show the authentication components. The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. ```tsx // highlight-next-line class App extends React.Component { render() { return ( {/*This renders the login UI on the route*/} // highlight-next-line {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ThirdPartyPreBuiltUI])} {/*Your app routes*/} ); } } ``` :::important If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample. Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details. ```tsx function AppRoutes() { const authRoutes = getSuperTokensRoutesForReactRouterDom( reactRouterDom, [/* Add your UI recipes here e.g. EmailPasswordPrebuiltUI, PasswordlessPrebuiltUI, ThirdPartyPrebuiltUI */] ); const routes = useRoutes([ ...authRoutes.map(route => route.props), // Include the rest of your app routes ]); return routes; } function App() { return ( ); } ``` ::: ```tsx // highlight-next-line class App extends React.Component { render() { // highlight-start if (canHandleRoute([ThirdPartyPreBuiltUI])) { // This renders the login UI on the route return getRoutingComponent([ThirdPartyPreBuiltUI]) } // highlight-end return ( {/*Your app*/} ); } } ``` Add the `ThirdParty` recipe in your `AuthComponent`. ```tsx title="/app/auth/auth.component.ts" @Component({ selector: "app-auth", template: '
', }) export class AuthComponent implements OnDestroy, AfterViewInit { constructor( private renderer: Renderer2, @Inject(DOCUMENT) private document: Document ) { } ngAfterViewInit() { this.loadScript('^{prebuiltUIVersion}'); } ngOnDestroy() { // Remove the script when the component is destroyed const script = this.document.getElementById('supertokens-script'); if (script) { script.remove(); } } private loadScript(src: string) { const script = this.renderer.createElement('script'); script.type = 'text/javascript'; script.src = src; script.id = 'supertokens-script'; script.onload = () => { supertokensUIInit({ appInfo: { // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ // highlight-start supertokensUIThirdParty.init({ signInAndUpFeature: { providers: [ supertokensUIThirdParty.Github.init(), supertokensUIThirdParty.Google.init(), supertokensUIThirdParty.Facebook.init(), supertokensUIThirdParty.Apple.init(), ] } }), // highlight-end supertokensUISession.init(), ], }); } this.renderer.appendChild(this.document.body, script); } } ```
Add the `ThirdParty` recipe in your `AuthView` file. ```tsx ```
#### Change the button style On the frontend, you can provide a button component to the in-built providers defining your own UI. The component you add is clickable by default. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { // highlight-start providers: [ Github.init({ buttonComponent: (props: {name: string}) =>
}), Google.init({ buttonComponent: (props: {name: string}) =>
}), Facebook.init({ buttonComponent: (props: {name: string}) =>
}), Apple.init({ buttonComponent: (props: {name: string}) =>
}), ], // highlight-end // ... }, // ... }), // ... ] }); ```
:::caution This is impossible for non-react apps at the moment. Please use custom UI instead for the sign-in form. :::
### 2. Initialize the backend SDK You have to initialize the **Backend Software Development Kit (SDK)** alongside the code that starts your server. The init call includes [configuration details](/docs/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. ```tsx title="Backend SDK Init" showAppTypeSelect // highlight-next-line supertokens.init({ // Replace this with the framework you are using framework: "express", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ // highlight-start ThirdParty.init({/*TODO: See next step*/}), // highlight-end Session.init() ] }); ``` ```python title="Backend SDK Init" showAppTypeSelect from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, session # highlight-next-line init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="", website_base_path="" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io^{derived.appIdPathname}", # api_key: ), framework='fastapi', recipe_list=[ session.init(), # initializes session features thirdparty.init( # TODO: See next step ) # type: ignore ], mode='asgi' # use wsgi if you are running using gunicorn ) ``` ```go title="Backend SDK Init" showAppTypeSelect ### 3. Add the authentication providers Populate the `providers` array with the third party authentication providers that you want. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ //highlight-start signInAndUpFeature: { // We have provided you with development keys which you can use for testing. // IMPORTANT: Please replace them with your own OAuth keys for production use. providers: [{ config: { thirdPartyId: "google", clients: [{ clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", clientSecret: "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" }] } }, { config: { thirdPartyId: "github", clients: [{ clientId: "467101b197249757c71f", clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd" }] } }, { config: { thirdPartyId: "apple", clients: [{ clientId: "4398792-io.supertokens.example.service", additionalConfig: { keyId: "7M48Y4RYDL", privateKey: "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", teamId: "YWQCXGJRJL", } }] } }], } //highlight-end }), // ... ] }); ``` ```go SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "google", clients: [{ clientId: "TODO: GOOGLE_CLIENT_ID", clientSecret: "TODO: GOOGLE_CLIENT_SECRET", scope: ["scope1", "scope2"] }] } } ] } }) ] }); ``` ```go
### 1. Initialize the frontend SDK Call the SDK init function at the start of your application. The invocation includes the [main configuration details](/docs/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you are using in your setup. ```tsx SuperTokens.init({ appInfo: { apiDomain: "", apiBasePath: "", appName: "...", }, recipeList: [ // highlight-next-line ThirdParty.init(), Session.init(), ], }); ``` First, you need to add the recipe script tag. ```html ``` You can initialize the SDK. ```tsx supertokens.init({ appInfo: { apiDomain: "", apiBasePath: "", appName: "...", }, recipeList: [ supertokensSession.init(), // highlight-next-line supertokensThirdParty.init(), ], }); ``` ```tsx SuperTokens.init({ apiDomain: "", apiBasePath: "", }); ``` Add the `SuperTokens.init` function call at the start of your application. ```kotlin void main() { SuperTokens.init( apiDomain: "", apiBasePath: "", ); } ``` ### 2. Add the login UI The **ThirdParty** flow involves creating a UI element that allows the user to initiate the login process. This occurs through a separate button for each provider that you have configured. You can have a look at the [UI implementation](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/thirdparty-auth--sign-in-up) to get a better idea. After the user clicks one of those buttons the actions that you need to take differ based on which type of authentication scenario you are using: - **Authorization Code** This option can either involve a **Client Secret** configured on the backend or rely on **Proof Key for Code Exchange (PKCE)** exchange. The difference between the two is that the first option uses a private secret, on the backend, to get the access token. Whereas the second one makes use of the **Proof Key for Code Exchange (PKCE)** flow to perform the token exchange. Regardless of which authentication type you are using, in the end, the access token fetches the user info and logs them in. - **OAuth/Access Tokens** This option only applies to mobile/desktop apps. The frontend obtains the Access Token and then sends it to the backend. SuperTokens then fetches user info using the access token and logs them in. #### Redirecting to a social/single sign-on provider The first step is to fetch the URL on which the user authenticates. You can do this by querying the backend API exposed by SuperTokens (as shown below). The backend SDK automatically appends the right query params to the URL (like scope, client ID etc). After getting the URL, redirect the user there. In the code below, an example of login with Google appears: ```tsx async function googleSignInClicked() { try { const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ thirdPartyId: "google", // This is where Google should redirect the user back after login or error. // This URL goes on the Google's dashboard as well. frontendRedirectURI: "http:///auth/callback/google", }); /* Example value of authUrl: https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&access_type=offline&include_granted_scopes=true&response_type=code&client_id=1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com&state=5a489996a28cafc83ddff&redirect_uri=https%3A%2F%2Fsupertokens.io%2Fdev%2Foauth%2Fredirect-to-app&flowName=GeneralOAuthFlow */ // we redirect the user to google for auth. window.location.assign(authUrl); } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function googleSignInClicked() { try { const authUrl = await supertokensThirdParty.getAuthorisationURLWithQueryParamsAndSetState({ thirdPartyId: "google", // This is where Google should redirect the user back after login or error. // This URL goes on the Google's dashboard as well. frontendRedirectURI: "http:///auth/callback/google", }); /* Example value of authUrl: https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&access_type=offline&include_granted_scopes=true&response_type=code&client_id=1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com&state=5a489996a28cafc83ddff&redirect_uri=https%3A%2F%2Fsupertokens.io%2Fdev%2Foauth%2Fredirect-to-app&flowName=GeneralOAuthFlow */ // we redirect the user to google for auth. window.location.assign(authUrl); } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` #### Handling the auth callback on your frontend Once the third party provider redirects your user back to your app, you need to consume the information to sign in the user. This requires you to: - Setup a route in your app that handles this callback. It's recommended to use something like `http:///auth/callback/google` (for Google). Regardless of what you make this path, remember to use that same path when calling the `getAuthorisationURLWithQueryParamsAndSetState` function in the first step. - On that route, call the following function on page load ```tsx async function handleGoogleCallback() { try { const response = await signInAndUp(); if (response.status === "OK") { console.log(response.user) if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // sign up successful } else { // sign in successful } window.location.assign("/home"); } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason) } else { // SuperTokens requires that the third party provider // gives an email for the user. If that's not the case, sign up / in // will fail. // As a hack to solve this, you can override the backend functions to create a fake email for the user. window.alert("No email provided by social login. Please use another form of login"); window.location.assign("/auth"); // redirect back to login page } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function handleGoogleCallback() { try { const response = await supertokensThirdParty.signInAndUp(); if (response.status === "OK") { console.log(response.user) if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // sign up successful } else { // sign in successful } window.location.assign("/home"); } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason) } else { // SuperTokens requires that the third party provider // gives an email for the user. If that's not the case, sign up / in // will fail. // As a hack to solve this, you can override the backend functions to create a fake email for the user. window.alert("No email provided by social login. Please use another form of login"); window.location.assign("/auth"); // redirect back to login page } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` :::note On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend SDK. ::: #### Special case for login with Apple Unlike other providers, Apple does not redirect your user back to your frontend app. Instead, it redirects the user to your backend with a `FORM POST` request. This means that the URL you configure on Apple's dashboard should point to your backend API layer. Here, **middleware** handles the request and redirects the user to your frontend app. Your frontend app should then call the `signInAndUp` API on that page as shown previously. To tell SuperTokens which frontend route to redirect the user back to, set the `frontendRedirectURI` to the frontend route. Also, set the `redirectURIOnProviderDashboard` to point to your backend API route, to which Apple sends a `POST` request. ```tsx async function appleSignInClicked() { try { const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ thirdPartyId: "apple", frontendRedirectURI: "http://localhost:3000/auth/callback/apple", // This is an example callback URL on your frontend. You can use another path as well. // highlight-start redirectURIOnProviderDashboard: "/callback/apple", // This URL goes on the Apple's dashboard // highlight-end }); // we redirect the user to apple for auth. window.location.assign(authUrl); } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function appleSignInClicked() { try { const authUrl = await supertokensThirdParty.getAuthorisationURLWithQueryParamsAndSetState({ thirdPartyId: "apple", frontendRedirectURI: "http://localhost:3000/auth/callback/apple", // This is an example callback URL on your frontend. You can use another path as well. // highlight-start redirectURIOnProviderDashboard: "/callback/apple", // This URL goes on the Apple's dashboard // highlight-end }); /* Example value of authUrl: https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&access_type=offline&include_granted_scopes=true&response_type=code&client_id=1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com&state=5a489996a28cafc83ddff&redirect_uri=https%3A%2F%2Fsupertokens.io%2Fdev%2Foauth%2Fredirect-to-app&flowName=GeneralOAuthFlow */ // we redirect the user to google for auth. window.location.assign(authUrl); } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` :::info If you are using the **Authorization Code Grant** flow with **PKCE** you do **not** need to provide a client secret during backend init. This only works for providers which support the [PKCE flow](https://oauth.net/2/pkce/). ::: ### Sign in with Apple example #### Fetching the authorisation token on the frontend For react native apps, this involves setting up the [react-native-apple-authentication library](https://github.com/invertase/react-native-apple-authentication) in your app. Check out their `README` for steps on how to integrate their `SDK` into your application. The minimum scope required by SuperTokens is the one that gives the user's email. In the case of Apple, that could be the user's actual email or the proxy email provided by Apple - it doesn't matter. Once the integration is complete, you should call the `appleAuth.performRequest` function for iOS and the `appleAuthAndroid.signIn` function for Android. Either way, the result of the function is a one-time use auth code which you should send to your backend as shown in the next step. A full example of this is available in [the example app](https://github.com/supertokens/supertokens-react-native/blob/master/examples/with-thirdparty/apple.ts). In case you are using Expo, you can use the [expo-apple-authentication](https://docs.expo.dev/versions/latest/sdk/apple-authentication/) library instead (not that this library only works on iOS). #### Fetching the authorisation token on the frontend :::info At the moment this flow is not supported on Android. ::: #### Fetching the authorisation token on the frontend For iOS you use the normal sign in with apple flow and then use the authorization code to login with SuperTokens. You can see a full example of this in the `onAppleClicked` function in [the example app](https://github.com/supertokens/supertokens-ios/blob/master/examples/with-thirdparty/with-thirdparty/LoginScreen/LoginScreenViewController.swift). ```swift void loginWithApple() async { try { var credential = await SignInWithApple.getAppleIDCredential( scopes: [ AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName, ], // Required for Android only webAuthenticationOptions: WebAuthenticationOptions( clientId: "", redirectUri: Uri.parse( "//callback/apple", ), ), ); String authorizationCode = credential.authorizationCode; String? idToken = credential.identityToken; String? email = credential.email; String? firstname = credential.givenName; String? lastName = credential.familyName; // Send the user information and auth code to the backend. Refer to the next step. } catch (e) { // Sign in aborted or failed } } ``` In the snippet above for Android, you need an additional `webAuthenticationOptions` property when signing in with Apple. This is because on Android the library uses the web login flow and requires the client id and redirection URI. The `redirectUri` property here is the URL to which Apple makes a `POST` request after the user has logged in. The SuperTokens backend SDKs provide an API for this at `//callback/apple`. #### Additional steps for Android For android, a way for the web login flow to redirect back to the app is also needed. By default, the API provided by the backend `SDKs` redirects to the website domain you provide when initializing the `SDK`. The API can be overridden to redirect to the app instead. For example, if using the Node.js `SDK`: ```tsx ThirdParty.init({ // highlight-start override: { apis: (original) => { return { ...original, appleRedirectHandlerPOST: async (input) => { if (original.appleRedirectHandlerPOST === undefined) { throw Error("Should never come here"); } // inut.formPostInfoFromProvider contains all the query params attached by Apple const stateInBase64 = input.formPostInfoFromProvider.state; // The web SDKs add a default state if (stateInBase64 === undefined) { // Redirect to android app // We create a dummy URL to create the query string const dummyUrl = new URL("http://localhost:8080"); for (const [key, value] of Object.entries(input.formPostInfoFromProvider)) { dummyUrl.searchParams.set(key, `${value}`); } const queryString = dummyUrl.searchParams.toString(); // Refer to the README of sign_in_with_apple to understand what this url is const redirectUrl = `intent://callback?${queryString}#Intent;package=YOUR.PACKAGE.IDENTIFIER;scheme=signinwithapple;end`; input.options.res.setHeader("Location", redirectUrl, false); input.options.res.setStatusCode(303); input.options.res.sendHTMLResponse(""); } else { // For the web flow we can use the original implementation original.appleRedirectHandlerPOST(input); } }, }; }, }, // highlight-end }) ``` ```go options.Res.Header().Set("Location", redirectUri) options.Res.WriteHeader(http.StatusSeeOther) return nil } else { return originalAppleRedirectPost(formPostInfoFromProvider, options, userContext) } } return originalImplementation }, }, // highlight-end }) } ``` ```python from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty.interfaces import APIInterface, APIOptions from typing import Dict, Any # highlight-start def override_thirdparty_apis(original_implementation: APIInterface): original_apple_redirect_post = original_implementation.apple_redirect_handler_post async def apple_redirect_handler_post( form_post_info: Dict[str, Any], api_options: APIOptions, user_context: Dict[str, Any] ): # form_post_info contains all the query params attached by Apple state = form_post_info["state"] # The web SDKs add a default state if state is None: query_items = [ f"{key}={value}" for key, value in form_post_info.items() ] query_string = "&".join(query_items) # Refer to the README of sign_in_with_apple to understand what this url is redirect_url = "intent://callback?" + query_string + "#Intent;package=YOUR.PACKAGE.IDENTIFIER;scheme=signinwithapple;end" api_options.response.set_header("Location", redirect_url) api_options.response.set_status_code(303) api_options.response.set_html_content("") else: return await original_apple_redirect_post(form_post_info, api_options, user_context) original_implementation.apple_redirect_handler_post = apple_redirect_handler_post return original_implementation # highlight-end thirdparty.init( # highlight-start override=thirdparty.InputOverrideConfig( apis=override_thirdparty_apis ), # highlight-end ) ``` In the code above, the `appleRedirectHandlerPOST` API is overridden to check if the request came from the Android app. You can skip checking the state if you only have a mobile app and no website. `sign_in_with_apple` requires parsing the query params sent by Apple and including them in the redirect URL in a specific way. Then, redirect to the deep link URL. Refer to the README for `sign_in_with_apple` to read about the deep link setup required in Android. #### Calling the `signinup` API to consume the authorisation token Once you have the authorization code from the auth provider, you need to call the `/signinup` API exposed by the backend `SDK` as shown below: ```bash curl --location --request POST '/signinup' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "thirdPartyId": "apple", "clientType": "...", "redirectURIInfo": { "redirectURIOnProviderDashboard": "/callback/apple", "redirectURIQueryParams": { "code": "...", "user": { "name":{ "firstName":"...", "lastName":"..." }, "email":"..." } } } }' ``` :::important - On iOS, the client id set in the backend should be the same as the bundle identifier for your app. - The `clientType` input is optional and required only if you initialize more than one client in the provider on the backend (See the "Social / `SSO` login for both, web and mobile apps" section below). - On iOS, `redirectURIOnProviderDashboard` doesn't matter and its value can be a universal link configured for your app. - On Android, the `redirectURIOnProviderDashboard` should match the one configured on the Apple developer dashboard. - The `user` object contains information provided by Apple. ::: The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This status occurs if the social / `SSO` provider did not provide an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you. ::: ### Sign in with Google example #### Fetching the authorisation token on the frontend This involves setting up the [@react-native-google-signin/google-signin](https://github.com/react-native-google-signin/google-signin) in your app. Checkout their `README` for steps on how to integrate their `SDK` into your application. The minimum scope required by SuperTokens is the one that gives the user's email. Once you configure the library, use `GoogleSignin.configure` and `GoogleSignin.signIn` to trigger the login flow and sign the user in with Google. Refer to [the example app](https://github.com/supertokens/supertokens-react-native/blob/master/examples/with-thirdparty/google.ts) to see the full code for this. ```tsx export const performGoogleSignIn = async (): Promise => { GoogleSignin.configure({ webClientId: "GOOGLE_WEB_CLIENT_ID", iosClientId: "GOOGLE_IOS_CLIENT_ID", }); try { const response = await GoogleSignin.signIn({}); const authCode = response.data?.serverAuthCode; // Refer to step 2 return true; } catch (e) { console.log("Google sign in failed with error", e); } return false; }; ``` #### Fetching the authorisation token on the frontend Follow the [official Google Sign In guide](https://developers.google.com/identity/sign-in/android/start-integrating) to set up their library and sign the user in with Google. Fetch the authorization code from the Google sign-in result. For a full example, refer to the `signInWithGoogle` function in [the example app](https://github.com/supertokens/supertokens-android/blob/master/examples/with-thirdparty/app/src/main/java/com/supertokens/supertokensexample/LoginActivity.kt). ```kotlin Future loginWithGoogle() async { GoogleSignIn googleSignIn; if (Platform.isAndroid) { googleSignIn = GoogleSignIn( serverClientId: "GOOGLE_WEB_CLIENT_ID", scopes: [ 'email', ], ); } else { googleSignIn = GoogleSignIn( clientId: "GOOGLE_IOS_CLIENT_ID", serverClientId: "GOOGLE_WEB_CLIENT_ID", scopes: [ 'email', ], ); } GoogleSignInAccount? account = await googleSignIn.signIn(); if (account == null) { print("Google sign in was aborted"); return; } String? authCode = account.serverAuthCode; if (authCode == null) { print("Google sign in did not return a server auth code"); return; } // Refer to step 2 } ```

Step 2) Calling the `signinup` API to consume the authorisation token

Once you have the authorization code from the auth provider, you need to call the `signinup` API exposed by the backend `SDK` as shown below: ```bash curl --location --request POST '/signinup' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "thirdPartyId": "google", "clientType": "...", "redirectURIInfo": { "redirectURIOnProviderDashboard": "", "redirectURIQueryParams": { "code": "...", } } }' ``` :::important When calling the API exposed by the SuperTokens backend `SDK`, pass an empty string for `redirectURIOnProviderDashboard`. The native login flow using the authorization code does not involve any redirection on the frontend. ::: The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This status occurs if the social / `SSO` provider did not provide an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you. ::: ### Authorization code grant flow with `PKCE` This is similar to the first one, except that you do **not** need to provide a client secret during backend init. This flow only works for providers which support the [`PKCE` flow](https://oauth.net/2/pkce/). #### Fetching the authorisation token on the frontend You can use the [react native auth library](https://github.com/FormidableLabs/react-native-app-auth) to also return the `PKCE` code verifier along with the authorization code. Achieve this by setting the `usePKCE` boolean to `true` and also by setting the `skipCodeExchange` to `true` when configuring the react native auth library. #### Fetching the authorisation token on the frontend You can use the [AppAuth-Android](https://github.com/openid/AppAuth-Android) library to use the `PKCE` flow by using the `setCodeVerifier` method when creating a `AuthorizationRequest`. #### Fetching the authorisation token on the frontend You can use the [AppAuth-iOS](https://github.com/openid/AppAuth-iOS) library to use the `PKCE` flow. #### Fetching the authorisation token on the frontend You can use [`flutter_appauth`](https://pub.dev/packages/flutter_appauth) to use the `PKCE` flow by providing a `codeVerifier` when you call the `appAuth.token` function. #### Calling the `signinup` API to consume the authorisation token Once you have the authorization code and `PKCE` verifier from the auth provider, you need to call the `/signinup` API exposed by the backend `SDK` as shown below: ```bash curl --location --request POST '/signinup' \ --header 'Content-Type: application/json' \ --data-raw '{ "thirdPartyId": "THIRD_PARTY_ID", "clientType": "...", "redirectURIInfo": { "redirectURIOnProviderDashboard": "REDIRECT_URI", "redirectURIQueryParams": { "code": "...", }, "pkceCodeVerifier": "..." } }' ``` :::important - Replace `THIRD_PARTY_ID` with the provider id. The provider id must match the one you configure in the backend when initialising SuperTokens. - `REDIRECT_URI` must exactly match the value you configure on the providers dashboard. ::: The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This status occurs if the social / `SSO` provider did not provide an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you. :::
:::info This flow is not applicable for web apps. ::: ### Fetching the OAuth/Access tokens on the frontend 1. Sign in with the social provider. The minimum required scope is the one that provides access to the user's email. You can use any library to sign in with the social provider. 2. Get the access token on the frontend if it is available. 3. Get the id token from the sign in result if it is available. :::important You need to provide either the access token or the id token, or both in step 2, depending on what is available. ::: ### Calling the `signinup` API to use the OAuth tokens Once you have the `access_token` or the `id_token` from the auth provider, you need to call the `/signinup` API exposed by the backend `SDK` as shown below: ```bash curl --location --request POST '/signinup' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "thirdPartyId": "google", "clientType": "...", "oAuthTokens": { "access_token": "...", "id_token": "..." }, }' ``` :::important - The `clientType` input is optional, and you need it only if you have initialised more than one client in the provide on the backend (See the "Social / Single Sign-On login for both, web and mobile apps" section below). - If you have the `id_token`, you can send that along with the `access_token`. ::: The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This occurs if the social / Single Sign-On provider did not provide an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you. :::
### 3. Initialize the backend SDK You have to initialize the **Backend Software Development Kit (SDK)** alongside the code that starts your server. The init call includes [configuration details](/docs/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. ```tsx title="Backend SDK Init" showAppTypeSelect // highlight-next-line supertokens.init({ // Replace this with the framework you are using framework: "express", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ // highlight-start ThirdParty.init({/*TODO: See next step*/}), // highlight-end Session.init() ] }); ``` ```python title="Backend SDK Init" showAppTypeSelect from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, session # highlight-next-line init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="", website_base_path="" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io^{derived.appIdPathname}", # api_key: ), framework='fastapi', recipe_list=[ session.init(), # initializes session features thirdparty.init( # TODO: See next step ) # type: ignore ], mode='asgi' # use wsgi if you are running using gunicorn ) ``` ```go title="Backend SDK Init" showAppTypeSelect ### 4. Add the authentication providers Populate the `providers` array with the third party authentication providers that you want. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ //highlight-start signInAndUpFeature: { // We have provided you with development keys which you can use for testing. // IMPORTANT: Please replace them with your own OAuth keys for production use. providers: [{ config: { thirdPartyId: "google", clients: [{ clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", clientSecret: "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" }] } }, { config: { thirdPartyId: "github", clients: [{ clientId: "467101b197249757c71f", clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd" }] } }, { config: { thirdPartyId: "apple", clients: [{ clientId: "4398792-io.supertokens.example.service", additionalConfig: { keyId: "7M48Y4RYDL", privateKey: "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", teamId: "YWQCXGJRJL", } }] } }], } //highlight-end }), // ... ] }); ``` ```go SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "google", clients: [{ clientId: "TODO: GOOGLE_CLIENT_ID", clientSecret: "TODO: GOOGLE_CLIENT_SECRET", scope: ["scope1", "scope2"] }] } } ] } }) ] }); ``` ```go
Having completed the main setup, you can explore more advanced topics related to the **ThirdParty** recipe. Built-in Providers Read more about the common providers exposed by the recipe. Custom Providers See how you can create your own custom provider. Custom Invite Flow Disable public sign ups and use your own invite flow. Hooks and Overrides Add custom logic after the logs in or signs up. # Authentication - Social Login - Built-in providers Source: https://supertokens.com/docs/authentication/social/built-in-providers This page shows a full list of all the built-in providers exposed by **SuperTokens**. ## Google To generate your client ID and secret follow the [official documentation](https://support.google.com/cloud/answer/6158849?hl=en). Set the authorisation callback URL to `/callback/google` ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "google", clients: [{ clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", clientSecret: "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", }] } } ] } }), // initializes signin / sign up features ] }); ``` ```go ## Google workspaces ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "google-workspaces", clients: [{ clientId: "TODO", clientSecret: "TODO", additionalConfig: { "hd": "example.com" } }] } } ] } }), // initializes signin / sign up features ] }); ``` ```go ## Apple To generate your client ID and secret follow [this article](https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003). Set the authorisation callback URL to `/callback/apple`. Note that Apple doesn't allow `localhost` in the URL. If you are in `dev` mode, you can use the `dev` keys provided above. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "apple", clients: [{ clientId: "4398792-io.supertokens.example.service", additionalConfig: { "keyId": "7M48Y4RYDL", "privateKey": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", "teamId": "YWQCXGJRJL", }, }] } } ] } }), // initializes signin / sign up features ] }); ``` ```go ## Discord ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "discord", clients: [{ clientId: "TODO", clientSecret: "TODO" }] } } ] } }), // initializes signin / sign up features ] }); ``` ```go ## Facebook ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "facebook", clients: [{ clientId: "TODO", clientSecret: "TODO" }] } } ] } }), // initializes signin / sign up features ] }); ``` ```go ## GitHub To generate your client ID and secret follow the [official documentation](https://docs.github.com/en/developers/apps/creating-an-oauth-app). Set the authorisation callback URL to `/callback/github` ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "github", clients: [{ clientId: "TODO", clientSecret: "TODO" }] } } ] } }), // initializes signin / sign up features ] }); ``` ```go ## GitLab ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "gitlab", clients: [{ clientId: "TODO", clientSecret: "TODO" }], oidcDiscoveryEndpoint: "https://gitlab.example.com/.well-known/openid-configuration", } } ] } }), // initializes signin / sign up features ] }); ``` ```go ## Twitter ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "twitter", clients: [{ clientId: "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", clientSecret: "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC" }] } } ] } }), // initializes signin / sign up features ] }); ``` ```go ## LinkedIn ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "linkedin", clients: [{ clientId: "TODO", clientSecret: "TODO" }] } } ] } }), // initializes signin / sign up features ] }); ``` ```go ## Okta ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "okta", clients: [{ clientId: "TODO", clientSecret: "TODO" }], oidcDiscoveryEndpoint: "https://dev-.okta.com/.well-known/openid-configuration", } } ] } }), // initializes signin / sign up features ] }); ``` ```go ## SAML ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "boxy-saml", name: "", // Replace with the correct provider name clients: [{ clientId: "TODO", clientSecret: "TODO", additionalConfig: { "boxyURL": "" } }], } } ] } }), // initializes signin / sign up features ] }); ``` ```go ## Active Directory ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "active-directory", clients: [{ clientId: "TODO", clientSecret: "TODO" }], oidcDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", } } ] } }), // initializes signin / sign up features ] }); ``` ```go Call the following function / API to add the third party provider to a specific tenant. ## Google To generate your client ID and secret follow the [official documentation](https://support.google.com/cloud/answer/6158849?hl=en) ```tsx async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "google", name: "Google", clients: [{ clientId: "...", clientSecret: "..." }] }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go ## Google workspaces ```tsx async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "google-workspaces", name: "Google Workspaces", clients: [{ clientId: "...", clientSecret: "...", additionalConfig: { "hd": "example.com", } }] }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go ## Apple To generate your client ID and secret follow [this article](https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003) Note that Apple doesn't allow `localhost` in the URL. If you are in `dev` mode, you can use the `dev` keys provided above. Call the following function / API to add the third party provider to a specific tenant. ```tsx async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "apple", name: "Apple", clients: [{ clientId: "...", additionalConfig: { "keyId": "...", "privateKey": "...", "teamId": "...", } }] }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go ## Discord ```tsx async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "discord", name: "Discord", clients: [{ clientId: "...", clientSecret: "...", }] }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go ## Facebook ```tsx async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "facebook", name: "Facebook", clients: [{ clientId: "...", clientSecret: "...", }] }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go ## GitHub To generate your client ID and secret follow the [official documentation](https://docs.github.com/en/developers/apps/creating-an-oauth-app) ```tsx async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "github", name: "GitHub", clients: [{ clientId: "...", clientSecret: "...", }] }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go ## GitLab ```tsx async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "gitlab", name: "GitLab", clients: [{ clientId: "...", clientSecret: "...", }], oidcDiscoveryEndpoint: "https://gitlab.example.com/.well-known/openid-configuration", }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go ## Twitter ```tsx async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "twitter", name: "Twitter", clients: [{ clientId: "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", clientSecret: "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", }] }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go ## LinkedIn ```tsx async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "linkedin", name: "LinkedIn", clients: [{ clientId: "...", clientSecret: "...", }] }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go ## Okta ```tsx async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "okta", name: "Okta", clients: [{ clientId: "...", clientSecret: "...", }], oidcDiscoveryEndpoint: "https://dev-.okta.com/.well-known/openid-configuration", }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go ## SAML ```tsx async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "boxy-saml", name: "", clients: [{ clientId: "...", clientSecret: "...", additionalConfig: { "boxyURL": "", } }], }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go ## Active Directory ```tsx async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "active-directory", name: "Active Directory", clients: [{ clientId: "...", clientSecret: "...", }], oidcDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go --- # Authentication - Social Login - ## Overview Source: https://supertokens.com/docs/authentication/social/custom-providers If you can't find a provider in [the built-in list](/docs/authentication/social/built-in-providers-config), you can add your own custom implementation as shown below. :::info Note If you think that SuperTokens should support this provider by default, make sure to let the team know [on GitHub](https://github.com/supertokens/supertokens-node/issues/88). ::: --- ## Create a custom provider ### 1. Render the authentication method in the authentication UI Include the provider in the `providers` array in the frontend SDK. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { // highlight-start providers: [ { id: "custom", name: "X", // Will display "Continue with X" // optional // you do not need to add a click handler to this as // we add it for you automatically. buttonComponent: (props: {name: string}) =>
{"Login with " + props.name}
} ], // highlight-end // ... }, // ... }), // ... ] }); ```
:::caution This is impossible for non-react apps at the moment. Please use custom UI instead for the sign in form. ::: :::caution This is impossible for non-react apps at the moment. Please use custom UI instead for the sign in form. :::
You need to build your own UI listing the buttons for each of the social login providers you want your users to use. See [the implementation details page](../../custom-ui/thirdparty-login) for what to do after a user clicks one of the buttons. ### 2. Configure the credentials You can define a custom provider in a couple of ways. The simplest method is to provide the configuration for the `AuthorizationEndpoint`, `TokenEndpoint`, and the mapping for how the user's ID and email from the provider's profile information endpoint. This appears below: ```tsx SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { // highlight-start providers: [{ config: { thirdPartyId: "custom", name: "Custom provider", clients: [{ clientId: "...", clientSecret: "...", scope: ["profile", "email"] }], authorizationEndpoint: "https://example.com/oauth/authorize", authorizationEndpointQueryParams: { "someKey1": "value1", "someKey2": null }, tokenEndpoint: "https://example.com/oauth/token", tokenEndpointBodyParams: { "someKey1": "value1", }, userInfoEndpoint: "https://example.com/oauth/userinfo", userInfoMap: { fromUserInfoAPI: { userId: "id", email: "email", emailVerified: "email_verified", } } } }] // highlight-end } }) ] }) ``` ```go Social/Enterprise providers Select **Add Custom Provider** option New Provider Fill in the details as shown below and click on **Save** OAuth2 provider | Configuration Field | Description | Example | |---|---|---| | `thirdPartyId` | Unique identifier for the provider | For Google: `"google"` | | `name` | Display name used on the frontend login button | Setting `"XYZ"` shows "Login using XYZ" | | `clients` | Array of client credentials for frontend clients | Multiple entries needed for web/mobile apps with different credentials. Include `clientType` if using multiple clients | | `AuthorizationEndpoint` | URL for user login | Google: `"https://accounts.google.com/o/oauth2/v2/auth"` | | `AuthorizationEndpointQueryParams` | Optional configuration to modify query parameters | Can add, modify, or remove (using null) query params | | `TokenEndpoint` | API endpoint for exchanging Authorization Code | Google: `"https://oauth2.googleapis.com/token"` | | `TokenEndpointBodyParams` | Optional configuration to modify request body | Can add, modify, or remove (using null) body params | | `UserInfoEndpoint` | API endpoint for getting user information | Google: `"https://www.googleapis.com/oauth2/v1/userinfo"` | | `UserInfoMap.FromUserInfoAPI` | Maps provider's JSON response fields to user info | Example mapping:
`userId: "id"`
`email: "email"`
`emailVerified: "email_verified"`
For nested values use: `userId: "user.id"` |
If the provider is Open ID Connect (OIDC) compatible, you can provide URL for the `OIDCDiscoverEndpoint` configuration. The backend SDK automatically discovers authorization endpoint, token endpoint and the user info endpoint by querying the `/.well-known/openid-configuration`. Below is an example of how to set the OIDC discovery endpoint: ```tsx SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { // highlight-start providers: [{ config: { thirdPartyId: "custom", name: "Custom provider", clients: [{ clientId: "...", clientSecret: "...", scope: ["profile", "email"] }], // highlight-start oidcDiscoveryEndpoint: "https://example.com/.well-known/openid-configuration", // highlight-end authorizationEndpointQueryParams: { "someKey1": "value1", "someKey2": null }, userInfoMap: { fromIdTokenPayload: { userId: "id", email: "email", emailVerified: "email_verified", } } } }] // highlight-end } }) ] }) ``` ```go Social/Enterprise providers Select **Add Custom Provider** option New Provider Fill in the details as shown below and click on **Save** OAuth2 provider - The configuration values are similar to the ones in the "Via OAuth endpoints" method. Please read that section to understand the `thirdPartyId`, `name`, `clients` configuration. - Unlike the "Via OAuth endpoints", you can obtain the user's info from the ID token payload using the configuration specified by you in the `UserInfoMap.FromIdTokenPayload` map. - You can also add the `UserInfoMap.FromUserInfoAPI` map as done in the "Via OAuth endpoints" section. SuperTokens auto merges the user information.
--- ## Handle non standard providers. Sometimes, one of the steps in the providers interaction may not be per a standard. Therefore, providing the configuration like shown above may not work. To handle this case, you can override any of the steps that happen during the OAuth exchange. For example, the API call made to get the user's profile info makes a `GET` call to the `UserInfoEndpoint` with the user's access token. If your provider requires a different method or requires multiple calls to different endpoints to get the profile info, then you can override the default implementation as shown below: ```tsx SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [{ config: { thirdPartyId: "custom", name: "Custom provider", clients: [{ clientId: "...", clientSecret: "...", scope: ["profile", "email"] }], authorizationEndpoint: "https://example.com/oauth/authorize", authorizationEndpointQueryParams: { "response_type": "token", // Changing an existing parameter "response_mode": "form", // Adding a new parameter "scope": null, // Removing a parameter }, tokenEndpoint: "https://example.com/oauth/token" }, // highlight-start override: (originalImplementation) => { return { ...originalImplementation, getUserInfo: async function (input) { // Call provider's APIs to get profile info // ... return { thirdPartyUserId: "...", email: { id: "...", isVerified: true }, rawUserInfoFromProvider: { fromUserInfoAPI: { "first_name": "...", "last_name": "..." }, } } } } } // highlight-end }] } }) ] }) ``` ```go 2. `GetAuthorisationRedirectURL` This function returns the full URL (along with query params) to which the user needs to navigate to log in. 3. `ExchangeAuthCodeForOAuthTokens` This function is responsible for exchanging one time use `Authorization Code` with the user's tokens, such as `Access Token`, `ID Token`, etc. 4. `GetUserInfo` This function is responsible for fetching the user information such as `UserId`, `Email` and `EmailVerified` using the tokens returned from the previous function. --- # Authentication - Social Login - Hooks and overrides Source: https://supertokens.com/docs/authentication/social/hooks-and-overrides **SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. The following sections describe how you can adjust the `thirdparty` recipe to your needs. Explore the [references pages](/docs/references) for a more in depth guide on hooks and overrides. ## Frontend hook This method gets fired, with the `SUCCESS` action, immediately after a successful sign in or sign up. Follow the code snippet to determine if the user is signing up or signing in. With this method you can fire events immediately after a successful sign in. You can use it to send analytics events. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ ThirdParty.init({ // highlight-start onHandleEvent: async (context) => { if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } } // highlight-end }), Session.init() ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIThirdParty.init({ // highlight-start onHandleEvent: async (context) => { if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } } // highlight-end }), supertokensUISession.init() ] }); ``` :::caution Not applicable This section is not applicable for custom UI since you are calling the `signInUp` API yourself anyway. You can do anything you want post `signIn` / `signUp` based on the result of the API call. ::: ## Backend override Overriding the `signInUp` function allows you to introduce your own logic for the sign in process. Use it to persist different types of data or trigger actions. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [/* ... */] }, // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, signInUp: async function (input) { // First we call the original implementation of signInUp. let response = await originalImplementation.signInUp(input); // Post sign up response, we check if it was successful if (response.status === "OK") { let { id, emails } = response.user; // This is the response from the OAuth 2 provider that contains their tokens or user info. let providerAccessToken = response.oAuthTokens["access_token"]; let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; if (input.session === undefined) { if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // TODO: Post sign up logic } else { // TODO: Post sign in logic } } } return response; } } } } // highlight-end }), Session.init({ /* ... */ }) ] }); ``` ```go provider_access_token = result.oauth_tokens["access_token"] print(provider_access_token) if result.raw_user_info_from_provider.from_user_info_api is not None: first_name = result.raw_user_info_from_provider.from_user_info_api[ "first_name" ] print(first_name) if session is None: if ( result.created_new_recipe_user and len(result.user.login_methods) == 1 ): print("New user was created") # TODO: Post sign up logic else: print("User already existed and was signed in") # TODO: Post sign in logic return result original_implementation.sign_in_up = sign_in_up return original_implementation # highlight-end init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ thirdparty.init( # highlight-start override=thirdparty.InputOverrideConfig( functions=override_thirdparty_functions ), # highlight-end sign_in_and_up_feature=thirdparty.SignInAndUpFeature(providers=[]), ) ], ) ``` --- # Authentication - Social Login - Add multiple clients for the same provider Source: https://supertokens.com/docs/authentication/social/add-multiple-clients-for-the-same-provider ## Overview If you use a third-party login method for your web and mobile app, then you might need to setup different Client ID/Secret for the same provider on the backend. For example, in case of Apple login, Apple gives you different client IDs for iOS login vs web & Android login (same client ID for web and Android). ## Before you start This guide assumes that you have already implemented the [EmailPassword recipe](/docs/authentication/email-password/introduction) and have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). ## Steps ### 1. Update the backend configuration Add more clients to the Apple.init on the backend. Each client would need to be uniquely identified, and you achieve this using the `clientType` string. For example, you can add one `clientType` for `web-and-android` and one for `ios`. ```tsx let providers: ProviderInput[] = [ { config: { thirdPartyId: "apple", clients: [{ clientType: "web-and-android", clientId: "...", additionalConfig: { "keyId": "...", "privateKey": "...", "teamId": "...", } }, { clientType: "ios", clientId: "...", additionalConfig: { "keyId": "...", "privateKey": "...", "teamId": "...", } }] } } ] ``` ```go SuperTokens.init({ appInfo: { apiDomain: "", apiBasePath: "", appName: "...", }, // highlight-next-line clientType: "web-and-android", recipeList: [/*...*/], }); ``` If you are using the pre-built UI SDK (SuperTokens-auth-react) as well, you can provide the `clientType` configuration to it as follows: ```tsx SuperTokens.init({ appInfo: { apiDomain: "", websiteDomain: "", apiBasePath: "", appName: "...", }, // highlight-next-line clientType: "web-and-android", recipeList: [/*...*/], }); ``` We pass in the `clientType` during the init call. ```tsx supertokens.init({ appInfo: { apiDomain: "", apiBasePath: "", appName: "...", }, // highlight-next-line clientType: "web-and-android", recipeList: [/*...*/], }); ``` When making calls to the APIs from your mobile app, the request body also takes a `clientType` prop as seen in the above API calls. # Authentication - Social Login - Implement a custom invite flow Source: https://supertokens.com/docs/authentication/social/custom-invite-flow ## Overview This guide shows you how to disable public sign ups to allow only certain people to access your app. From a third-party login perspective, you need to maintain an allow list of emails and validate the users based on it. ## Before you start The tutorial assumes that you already have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). ### Prerequisites This guide uses the `UserMetadata` recipe to store the allow list. You need to [enable it](/docs/post-authentication/user-management/user-metadata) in the SDK initialization step. ## Steps ### 1. Implement the allow list You can store this list in your own database, or use the metadata feature provided by SuperTokens to store this. This may seem like a strange use case of the user metadata recipe provided, but it works. The following code samples show you how to save the allow list in the user metadata. ```tsx async function addEmailToAllowlist(email: string) { let existingData = await UserMetadata.getUserMetadata("emailAllowList"); let allowList: string[] = existingData.metadata.allowList || []; allowList = [...allowList, email]; await UserMetadata.updateUserMetadata("emailAllowList", { allowList }); } async function isEmailAllowed(email: string) { let existingData = await UserMetadata.getUserMetadata("emailAllowList"); let allowList: string[] = existingData.metadata.allowList || []; return allowList.includes(email); } ``` ```go ::: ### 2. Check if the email is allowed Update the backend SDK API function to only allow sign up requests from users that are on the allow list. To do this you need to use the check functions from the previous code snippet. ```tsx declare let isEmailAllowed: (email: string) => Promise // REMOVE_FROM_OUTPUT ThirdParty.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, signInUp: async function (input) { let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { email: input.email }); if (existingUsers.length === 0) { // this means that the email is new and is a sign up if (!(await isEmailAllowed(input.email))) { // email is not in allow list, so we disallow throw new Error("No sign up") } } // We allow the sign in / up operation return originalImplementation.signInUp(input); } } }, apis: (originalImplementation) => { return { ...originalImplementation, signInUpPOST: async function (input) { try { return await originalImplementation.signInUpPOST!(input); } catch (err: any) { if (err.message === "No sign up") { // this error was thrown from our function override above. // so we send a useful message to the user return { status: "GENERAL_ERROR", message: "Sign ups are disabled. Please contact the admin." } } throw err; } } } } } }) ``` ```go return false, nil } func main() { thirdparty.Init(&tpmodels.TypeInput{ Override: &tpmodels.OverrideStruct{ Functions: func(originalImplementation tpmodels.RecipeInterface) tpmodels.RecipeInterface { ogThirdPartySignInUp := *originalImplementation.SignInUp (*originalImplementation.SignInUp) = func(thirdPartyID, thirdPartyUserID, email string, oAuthTokens map[string]interface{}, rawUserInfoFromProvider tpmodels.TypeRawUserInfoFromProvider, tenantId string, userContext supertokens.UserContext) (tpmodels.SignInUpResponse, error) { existingUsers, err := thirdparty.GetUsersByEmail(tenantId, email) if err != nil { return tpmodels.SignInUpResponse{}, err } if len(existingUsers) == 0 { // this means that the email is new and is a sign up allowed, err := isEmailAllowed(email) if err != nil { return tpmodels.SignInUpResponse{}, err } if !allowed { return tpmodels.SignInUpResponse{}, errors.New("No sign up") } } // We allow the sign in / up operation return ogThirdPartySignInUp(thirdPartyID, thirdPartyUserID, email, oAuthTokens, rawUserInfoFromProvider, tenantId, userContext) } return originalImplementation }, APIs: func(originalImplementation tpmodels.APIInterface) tpmodels.APIInterface { originalSignInUpPOST := *originalImplementation.SignInUpPOST (*originalImplementation.SignInUpPOST) = func(provider *tpmodels.TypeProvider, input tpmodels.TypeSignInUpInput, tenantId string, options tpmodels.APIOptions, userContext supertokens.UserContext) (tpmodels.SignInUpPOSTResponse, error) { resp, err := originalSignInUpPOST(provider, input, tenantId, options, userContext) if err.Error() == "No sign up" { // this error was thrown from our function override above. // so we send a useful message to the user return tpmodels.SignInUpPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "Sign ups are disabled. Please contact the admin.", }, }, nil } return resp, err } return originalImplementation }, }, }) } ``` ```python from typing import Any, Dict, Optional, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.recipe import thirdparty from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.thirdparty.interfaces import ( APIInterface, APIOptions, RecipeInterface, ) from supertokens_python.recipe.thirdparty.provider import Provider, RedirectUriInfo from supertokens_python.recipe.thirdparty.types import RawUserInfoFromProvider from supertokens_python.types import GeneralErrorResponse from supertokens_python.types.base import AccountInfoInput async def is_email_allowed(email: str): # from previous code snippet.. return False def override_thirdparty_functions(original_implementation: RecipeInterface): original_thirdparty_sign_in_up = original_implementation.sign_in_up async def thirdparty_sign_in_up( third_party_id: str, third_party_user_id: str, email: str, is_verified: bool, oauth_tokens: Dict[str, Any], raw_user_info_from_provider: RawUserInfoFromProvider, session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, user_context: Dict[str, Any], ): existing_users = await list_users_by_account_info( tenant_id, AccountInfoInput(email=email) ) if len(existing_users) == 0: if not await is_email_allowed(email): raise Exception("No sign up") # this means this email is new so we allow sign up return await original_thirdparty_sign_in_up( third_party_id, third_party_user_id, email, is_verified, oauth_tokens, raw_user_info_from_provider, session, should_try_linking_with_session_user, tenant_id, user_context, ) raise Exception("No sign up") original_implementation.sign_in_up = thirdparty_sign_in_up return original_implementation def override_thirdparty_apis(original_implementation: APIInterface): original_sign_in_up_post = original_implementation.sign_in_up_post async def thirdparty_sign_in_up_post( provider: Provider, redirect_uri_info: Optional[RedirectUriInfo], oauth_tokens: Optional[Dict[str, Any]], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): try: return await original_sign_in_up_post( provider, redirect_uri_info, oauth_tokens, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) except Exception as e: if str(e) == "No sign up": return GeneralErrorResponse( "Seems like you already have an account with another method. Please use that instead." ) raise e original_implementation.sign_in_up_post = thirdparty_sign_in_up_post return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ thirdparty.init( override=thirdparty.InputOverrideConfig( apis=override_thirdparty_apis, functions=override_thirdparty_functions ), ) ], ) ``` # Authentication - Enterprise Login - Important concepts Source: https://supertokens.com/docs/authentication/enterprise/important-concepts ## Overview [Multitenancy](https://supertokens.com/features/multi-tenancy) is a way to organize your application to support multiple groups of users who share a common access with to an application. The user groups, or **tenants**, isolate from one another and can only access their specific data. Furthermore, each tenant can have different methods of logging in, configured by the tenant, or by you (the application developer). For example, a SaaS application for a financial company may want to separate their users by the financial institution they represent. This would require a login screen that asks for a username and password, as well as the name of the tenant. The application would then route the user to their specific tenant, which could be a different database or a different collection of data within a database. ## References With SuperTokens, there are two levels of abstractions for multi tenancy: **Tenant** and **Application**. ### Tenant A tenant is a group of users who share a common access with specific privileges to the application. Key characteristics: - Each tenant can have its own login method. For example, one tenant can have email password login, while another can have SSO login. - Each tenant has its own user pool. One user can login using the same email across different tenants, and the system treats them as different users. You can also share a user across tenants. - This feature also allows implementing data isolation for each of your tenants wherein a different database serves each tenant. - Roles and permissions exist on an app level, but the system defines their mapping to the users on a tenant level. This means that the same user shared across tenants can have different roles / permissions, depending on the tenant they log into. It also means that you can share the same role / permission set across tenants. - Sessions are per tenant (`appId` -> `tenantId` -> session handle) and cannot be shared across tenants. - For multiple tenants, you can run the same backend and frontend across all tenants of an app. Each request from the frontend contains a `tenantId` identifying that tenant to the backend, and once logged in, each session also contains that user's `tenantId`. ### Application The top-most level of abstraction in SuperTokens multitenancy. Key characteristics: - Each app can have its own set of tenants and users, which can't be shared with other apps. - Each app needs to have its own SuperTokens backend and SuperTokens frontend SDK setup. - User metadata is per app level since users are also on a per app level. - When you start the core for the first time, SuperTokens creates an app (`appId` is `"public"`) and one tenant in it (tenantId is `"public"`). When you create a new app, you also get a new tenant (`tenantId` is `"public"`) as part of that app created for you. - A user can be uniquely recognized by their `appId` -> `userId`. This allows the same user to share across tenants (if you want that to happen). - The identity of the user (their email for example) can be uniquely identified by `appId` -> `tenantId` -> email. This allows the same email to use across tenants in a way that they are still treated as different users (they have different user IDs). The same holds true for phone numbers and third party login profile. - You can create multiple apps and tenants in the same database or in different databases. The only restriction is that for an app, you cannot share a user across `tenantA` and `tenantB` if the databases for `tenantA` and `tenantB` are different. Putting it another way, a user can only share across the tenants if those tenants use the same database. ## Types of setup Based on the above abstractions you can choose between four different setup types when setting up authentication with **SuperTokens**. ### Single tenant, single app The default use case when you are not making use of the multi tenancy feature. ![Single tenant single app architecture](/img/single-tenant-single-app-diagram.svg) ### Single tenant, multi app This is where you have multiple applications running on the same SuperTokens core instance and each application has a single user pool. This could be two different apps in your organization, or two different development environments for the same app (or some combination of this). ![Single tenant multi app architecture](/img/single-tenant-multi-app-diagram.svg) ### Multi tenant, single app Different customers use the same application, but each customer has their own set of users and login methods (each customer is a unique tenant in SuperTokens). ![Multi tenant single app architecture](/img/multi-tenant-single-app-diagram.svg) ### Multi tenant, multi app Multiple applications run on the same **SuperTokens** core instance, and each application has its own set of tenants. This could be two different applications in your organization, or two different development environments for the same application (or some combination of this). ![Multi tenant multi app architecture](/img/multi-tenant-multi-app-diagram.svg) In a multi app, multi tenant setup: A user can be uniquely recognized by their `appId` -> `userId`. This allows the same user to share across tenants (if you want that to happen). The identity of the user (their email for example) can be uniquely identified by `appId` -> `tenantId` -> email. This allows the same email to use across tenants in a way that they are still treated as different users (they have different user IDs). The same holds true for phone numbers and third party login profile. Roles and permissions exist on an app level, but the system defines their mapping to the users on a tenant level. This means that the same user shared across tenants can have different roles / permissions, depending on the tenant they log into. It also means that you can share the same role / permission set across tenants. Sessions are per tenant (`appId` -> `tenantId` -> session handle) and cannot be shared across tenants. User metadata is per app level since users are also on a per app level. # Authentication - Enterprise Login - Initial setup Source: https://supertokens.com/docs/authentication/enterprise/initial-setup ## Before you start These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). If you have skipped that page please follow the tutorial and return here once you're done. ## Steps ### 1. Create a tenant The first step in setting up a multi tenant login system is to create a tenant in the SuperTokens core. Each tenant has a unique `tenantId` mapped to that tenant's configuation. The `tenantId` could be that tenant's sub domain, or a workspace URL, or anything else that can help identify them. The configuration mapped to each tenant contains information about which login methods they enable. ```tsx async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"] }); if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } } ``` The snippet creates a new tenant with the id `"customer1"`. It enables the email password, third party and passwordless login methods for this tenant. You can also disable any of these by not including them in the `firstFactors` input. If `firstFactors` is not specified, by default, the system does not enable any of the login methods. If you set `firstFactors` to `null` the SDK uses any of the login methods. The built-in Factor IDs available for `firstFactors` include: | Authentication Type | Factor ID | |-------------------|-----------| | Email password auth | `emailpassword` | | Social login / enterprise SSO auth | `thirdparty` | | Passwordless - Email OTP | `otp-email` | | Passwordless - SMS OTP | `otp-phone` | | Passwordless - Email magic link | `link-email` | | Passwordless - SMS magic link | `link-phone` | ```go #### Configure third party providers If you are using the `thirdparty` recipe on a tenant, you also need to set the providers that you want to use with it. There's an extensive list of [built-in providers](/docs/authentication/social/built-in-providers-config), but you can also configure [a custom provider](/docs/authentication/enterprise/manage-tenants). The next code snippet shows how you can add an Active Directory login to your tenant. Update the `clientId`, `clientSecret`, and `directoryId` based on your tenant configuration. ```tsx async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "active-directory", name: "Active Directory", clients: [{ clientId: "...", clientSecret: "...", }], oidcDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go ### 2. Provide additional configuration per tenant You can also configure a tenant to use different settings. The next sample shows you how to customize the values. ```tsx async function createNewTenant() { // highlight-start let resp = await Multitenancy.createOrUpdateTenant("customer1", { coreConfig: { "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2", } }); // highlight-end if (resp.createdNew) { // new tenant was created } else { // existing tenant's config was modified. } } ``` In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. ```go Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate async def some_func(): tenant_id = "customer1" result = await create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate( core_config={ "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2", }, )) if result.status != "OK": print("handle error") elif result.created_new: print("new tenant created") else: print("existing tenant's config was modified.") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate tenant_id = "customer1" result = create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate( core_config={ "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2", }, )) if result.status != "OK": print("handle error") elif result.created_new: print("new tenant created") else: print("existing tenant's config was modified.") ``` In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. ```bash curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data-raw '{ "tenantId": "customer1", "coreConfig": { "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2" } }' ``` In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. Custom tenant configuration In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. You can edit the values by clicking on the pencil icon and then specifying a new value. :::caution You cannot edit database connection settings directly from the Dashboard, and you may need to use the SDK or cURL to update them. ::: ### 3. View tenant details To view the configuration for a specific tenant you can use an SDK method or call the API directly. ```tsx async function getTenant(tenantId: string) { // highlight-start let resp = await Multitenancy.getTenant(tenantId); // highlight-end if (resp === undefined) { // tenant does not exist } else { let coreConfig = resp.coreConfig; let firstFactors = resp.firstFactors; let configuredThirdPartyProviders = resp.thirdParty.providers; } } ``` ```go isThirdPartyLoginEnabled := tenant.ThirdParty.Enabled; isPasswordlessLoginEnabled := tenant.Passwordless.Enabled; if (isEmailPasswordLoginEnabled) { // Tenant support email password login } if (isThirdPartyLoginEnabled) { // Tenant support third party login configuredThirdPartyProviders := tenant.ThirdParty.Providers; fmt.Println(configuredThirdPartyProviders); } if (isPasswordlessLoginEnabled) { // Tenant support passwordless login } } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import get_tenant async def some_func(): tenant = await get_tenant("customer1") if tenant is None: print("tenant does not exist") else: core_config = tenant.core_config first_factors = tenant.first_factors providers = tenant.third_party_providers print(core_config) print(first_factors) print(providers) ``` ```python from supertokens_python.recipe.multitenancy.syncio import get_tenant tenant = get_tenant("customer1") if tenant is None: print("tenant does not exist") else: core_config = tenant.core_config first_factors = tenant.first_factors providers = tenant.third_party_providers print(core_config) print(first_factors) print(providers) ``` ```bash curl --location --request GET 'http://localhost:3567/customer1/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' ``` Notice that you add `customer1` to the path of the request. This tells the core that the tenant you want to get the information about is `customer1` (the one created before in this page). If the input tenant does not exist, you get back a `200` status code with the following JSON: ```json {"status": "TENANT_NOT_FOUND_ERROR"} ``` Otherwise you get a `200` status code with the following JSON output: ```json { "status": "OK", "thirdParty": { "providers": [...] }, "coreConfig": { "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2" }, "tenantId": "customer1", "firstFactors": ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-email", "link-phone"] } ``` The returned `coreConfig` is the same as what you set when creating / updating the tenant. The rest of the core configurations for this tenant inherit from the app's (or the `public` tenant) configuration. The `public` tenant, for the `public` app inherits its configurations from the `config.yaml` / docker environment variables values. ### 4. Set up the user interface To allow users to authenticate using one of your previously created tenants you need to update your frontend application. You can do this in two ways: [through a common domain](/docs/authentication/enterprise/common-domain-login), [through subdomains](/docs/authentication/enterprise/subdomain-login). Explore the two guides for a full list of instructions on how to implement the flows. # Authentication - Enterprise Login - Common domain login Source: https://supertokens.com/docs/authentication/enterprise/common-domain-login ## Overview This guide shows you how to authenticate users through the same page, `https://example.com/auth`, and then redirect to their sub domain after login. The login page adjusts the authentication method based on the tenant's configuration. You can figure out which is the tenant that you are working with through different methods. A common way is to ask the user to enter their organisation name, which is equal to the `tenantId` that you configure in SuperTokens. :::info Important You can find an example app for this setup with the **pre-built UI** on [the GitHub example directory](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-one-login-many-subdomains). The app is setup to have three tenants: - `tenant1`: Login with `emailpassword` + Google sign in - `tenant2`: Login with `emailpassword` - `tenant3`: Login with passwordless + GitHub sign in You can also generate a demo app using the following command: ```bash npx create-supertokens-app@latest --recipe=multitenancy ``` ::: ## Before you start The tutorial assumes that you already have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). You also need to create the tenants that your application requires. View the [previous tutorial](/docs/authentication/enterprise/initial-setup) for more information on how to do this. ## Steps ### 1. Ask for the tenant ID on the login page If you have [followed the pre-built UI setup](/docs/quickstart/frontend-setup), when you visit the login screen, you see the login screen immediately. The flow needs to change to first ask the user to enter their tenant ID and then display the login UI based on the tenant ID. To do that, first obtain the tenant ID from the user. You can achieve this by building a UI that asks them to enter their `tenantId` or Organisation name (which can serve as the tenant ID). This UI builds in a component called `AuthPage`. :::warning no-title You have to [create tenants](/docs/authentication/enterprise/initial-setup) before you can complete this step. ::: ```tsx export const AuthPage = () => { const location = reactRouterDom.useLocation(); const [inputTenantId, setInputTenantId] = useState(""); const tenantId = localStorage.getItem("tenantId") ?? undefined; const session = useSessionContext(); if (session.loading) { return null; } if ( tenantId !== undefined || // if we have a tenantId stored session.doesSessionExist === true || // or an active session (it'll contain the tenantId) new URLSearchParams(location.search).has("tenantId") // or we are on a link (e.g.: email verification) that contains the tenantId ) { // Since this component (AuthPage) is rendered in the route in the main Routes component, // and we are rendering this in a sub route as shown below, the third arg to getSuperTokensRoutesForReactRouterDom // tells SuperTokens to create Routes without prefix to them, otherwise they would // render on ^{form_websiteBasePath} path. return ( {getSuperTokensRoutesForReactRouterDom( reactRouterDom, [EmailPasswordPreBuiltUI], "" )} ); } else { return (
{ // this value will be read by SuperTokens as shown in the next steps. localStorage.setItem("tenantId", inputTenantId); }}>

Enter your organisation's name:

setInputTenantId(e.target.value)} />
); } }; ```
```tsx export const AuthPage = () => { const [inputTenantId, setInputTenantId] = useState(""); const tenantId = localStorage.getItem("tenantId") ?? undefined; const session = useSessionContext(); if (session.loading) { return null; } if ( tenantId !== undefined || // if we have a tenantId stored session.doesSessionExist === true || // or an active session (it'll contain the tenantId) new URLSearchParams(location.search).has("tenantId") // or we are on a link (e.g.: email verification) that contains the tenantId ) { return getRoutingComponent([EmailPasswordPreBuiltUI]); } else { return (
{ // this value will be read by SuperTokens as shown in the next steps. localStorage.setItem("tenantId", inputTenantId); }}>

Enter your organisation's name:

setInputTenantId(e.target.value)} />
); } }; ```
The example creates a simple UI renders which asks the user for their organisation's name. Their input serves as their tenant ID. When the user submits that form, the value stores in `localstorage`. :::info Important The `AuthPage` component should render to show on `/*` paths of the website. The `AuthPage` replaces the call to `getSuperTokensRoutesForReactRouterDom` or `getRoutingComponent` that you may have added to your app from the quick setup section. :::
:::info Caution No code snippet provided here, however, if you visit the auth component, you see that the pre-built UI renders in the `"supertokensui"` `div` element on page load. The logic here needs to change to first check if the user has provided the `tenantId`. If they have, the SuperTokens UI renders as usual. If they have not, a simple UI renders which asks the user for their tenant id and saves that in `localstorage`. Switch to the React code tab here to see the implementation in React, and a similar logic applies here. :::
You need to build a UI that asks the user to enter their `tenantId` or Organisation name (which can serve as the tenant ID). The input value is then utilized in function calls, as seen below. Once you have the user's tenant ID, you can fetch their list of configured providers and render the third party login buttons accordingly: ```tsx async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { const loginMethods = await Multitenancy.getLoginMethods({ tenantId }) if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button } else { // more checks for other providers } } else { // thirdparty login is disabled for the tenant } } ``` ```tsx async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { const loginMethods = await Multitenancy.getLoginMethods({ tenantId }) if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button } else { // more checks for other providers } } else { // thirdparty login is disabled for the tenant } } ``` - In the code snippet above, the login methods for the `tenantId` fetch. - We then render the login UI buttons based on the configured `thirdPartyId`s in the response ```bash curl --location --request GET '/loginmethods' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: The `recipes` field contains information about which login methods are active along with the list of third party providers configured for this tenant. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should appear on the frontend. ### 2. Include the tenant ID in authentication flow You need to tell SuperTokens how to resolve the tenant ID. To do this, set the `getTenantId` function in the `Multitenancy` recipe. In the current example, local storage provides the `tenantId`. ```tsx SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", apiBasePath: "...", websiteBasePath: "..." }, // highlight-next-line usesDynamicLoginMethods: true, recipeList: [ // highlight-start Multitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: (input) => { let tid = localStorage.getItem("tenantId"); return tid === null ? undefined : tid; } } } } }) // highlight-end // other recipes... ] }); ``` :::info Important Set the `usesDynamicLoginMethods` to `true` to tell SuperTokens that the login methods are dynamic (based on the `tenantId`). On page load (of the login page), SuperTokens first fetches the configured login methods for the `tenantId`. It then displays the login UI based on the result of the API call. ::: ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", apiBasePath: "...", websiteBasePath: "..." }, // highlight-next-line usesDynamicLoginMethods: true, recipeList: [ // highlight-start supertokensUIMultitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: (input) => { let tid = localStorage.getItem("tenantId"); return tid === null ? undefined : tid; } } } } }) // highlight-end // other recipes... ] }); ``` :::info Important Set the `usesDynamicLoginMethods` to `true` to tell SuperTokens that the login methods are dynamic (based on the `tenantId`). On page load (of the login page), SuperTokens first fetches the configured login methods for the `tenantId`. It then displays the login UI based on the result of the API call. ::: Initialize the multi tenancy recipe with the following callback defined. You can get the value of `tenantId` from wherever you had stored it when asking the user for its value. ```tsx SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", }, recipeList: [ // highlight-start Multitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: (input) => { let tid = localStorage.getItem("tenantId"); return tid === null ? undefined : tid; } } } } }) // highlight-end // other recipes... ] }); ``` ```tsx supertokens.init({ appInfo: { appName: "...", apiDomain: "...", }, recipeList: [ // highlight-start supertokensMultitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: (input) => { let tid = localStorage.getItem("tenantId"); return tid === null ? undefined : tid; } } } } }) // highlight-end // other recipes... ] }); ``` All the steps for mobile app login are similar to the [social login steps](../../custom-ui/thirdparty-login). However, when you are calling the sign in up API, you also need to pass in the `tenantId` in the request path. An example of this appears below: ```bash curl --location --request POST '/signinup' \ --header 'Content-Type: application/json' \ --data-raw '{ "thirdPartyId": "...", "clientType": "...", "oAuthTokens": { "access_token": "...", "id_token": "..." }, }' ``` ### 3. Redirect users based on tenant subdomain {{optional}} If each tenant has access to specific sub domains in your application you need to redirect them after login. #### 3.1 Restrict subdomain access Before you perform the actual redirect step, you should restrict which sub domains they have access to. To do this configure the SDK to know which domain each `tenantId` has access to. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ // highlight-start Multitenancy.init({ getAllowedDomainsForTenantId: async (tenantId, userContext) => { // query your db to get the allowed domain for the input tenantId // or you can make the tenantId equal to the sub domain itself return [tenantId + ".myapp.com", "myapp.com", "www.myapp.com"] } }), // highlight-end // other recipes... ] }) ``` ```go SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, // highlight-start getRedirectionURL: async (context) => { if (context.action === "SUCCESS" && context.newSessionCreated) { let claimValue: string[] | undefined = await Session.getClaimValue({ claim: Multitenancy.AllowedDomainsClaim }); if (claimValue !== undefined) { window.location.href = "https://" + claimValue[0]; } else { // there was no configured allowed domain for this user. Throw an error cause of // misconfig or redirect to a default sub domain } } return undefined; }, // highlight-end recipeList: [ /* Recipe init here... */ ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, // highlight-start getRedirectionURL: async (context) => { if (context.action === "SUCCESS" && context.newSessionCreated) { let claimValue: string[] | undefined = await supertokensUISession.getClaimValue({ claim: supertokensUIMultitenancy.AllowedDomainsClaim }); if (claimValue !== undefined) { window.location.href = "https://" + claimValue[0]; } else { // there was no configured allowed domain for this user. Throw an error cause of // misconfig or redirect to a default sub domain } } return undefined; }, // highlight-end recipeList: [ /* Recipe init here... */ ] }); ``` On the frontend, once the user has completed login, you can read the domain that they have access to (from their session) and redirect them accordingly. ```tsx async function redirectToSubDomain() { if (await Session.doesSessionExist()) { let claimValue: string[] | undefined = await Session.getClaimValue({ claim: Multitenancy.AllowedDomainsClaim }); if (claimValue !== undefined) { window.location.href = "https://" + claimValue[0]; } else { // there was no configured allowed domain for this user. Throw an error cause of // misconfig or redirect to a default sub domain } } else { window.location.href = "/auth"; } } ``` ```tsx async function redirectToSubDomain() { if (await supertokensSession.doesSessionExist()) { let claimValue: string[] | undefined = await supertokensSession.getClaimValue({ claim: supertokensMultitenancy.AllowedDomainsClaim }); if (claimValue !== undefined) { window.location.href = "https://" + claimValue[0]; } else { // there was no configured allowed domain for this user. Throw an error cause of // misconfig or redirect to a default sub domain } } else { window.location.href = "/auth"; } } ``` - The `AllowedDomainsClaim` claim is auto added to the session by the backend SDK if you provide the `GetAllowedDomainsForTenantId` configuration from the previous step. - This claim contains a list of domains that this user can access, based on their tenant ID. ### 6. Share sessions across sub domains {{optional}} If the user authenticates into your main website domain (`https://example.com/auth`), and redirects to a sub domain, the session recipe needs updating. It should allow sharing of sessions across sub domains. You can do this [by setting the `sessionTokenFrontendDomain` value in the Session recipe](/docs/post-authentication/session-management/share-session-across-sub-domains). If the sub domains assigned to your tenants have their own backend on a separate sub domain (one per tenant), you can also enable [sharing of sessions across API domains](/docs/post-authentication/session-management/advanced-workflows/multiple-api-endpoints). ### 7. Limit user access to their sub domain. {{optional}} The frontend uses session claim validators to restrict sub domain access. Before proceeding, make sure that you define the `GetAllowedDomainsForTenantId` function mentioned above. This adds the list of allowed domains into the user's access token payload. On the frontend, it is necessary to check if the tenant has access to the current sub domain. If not, they should redirect to the right sub domain. You can achieve this by using the `hasAccessToCurrentDomain` session validator from the multi tenancy recipe. ```tsx Session.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await Session.getClaimValue({ claim: AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }) ``` Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](/docs/additional-verification/session-verification/protect-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to their right sub domain (via the values set in the `AllowedDomainsClaim` session claim). You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUISession.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await supertokensUISession.getClaimValue({ claim: supertokensMultitenancy.AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }) ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx Session.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await Session.getClaimValue({ claim: AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }) ``` Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](../sessions/protecting-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to their right sub domain (via the values set in the `AllowedDomainsClaim` session claim). ```tsx Session.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await Session.getClaimValue({ claim: AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }) ``` ```tsx supertokensSession.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await supertokensSession.getClaimValue({ claim: supertokensMultitenancy.AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }) ``` Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](../sessions/protecting-frontend-routes#verifying-the-claims-of-a-session--cust), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to their right sub domain (via the values set in the `AllowedDomainsClaim` session claim). --- # Authentication - Enterprise Login - Subdomain login Source: https://supertokens.com/docs/authentication/enterprise/subdomain-login ## Overview This guide shows you how to authenticate users through different subdomains. The authentication method that displayed on each page varies based on tenant configuration. :::important Throughout this page, assume that the tenant ID for a tenant is equal to their sub domain. If the sub domain assigned to a tenant is `customer1.example.com`, then their `tenantId` is `customer1`. An example app for this setup with the **pre-built UI** is available on [the GitHub example directory](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-one-login-per-subdomain). The app is setup to have three tenants: - `tenant1.example.com`: Login with `emailpassword` + Google sign in - `tenant2.example.com`: Login with `emailPassword` - `tenant3.example.com`: Login with passwordless + GitHub sign in ::: ## Before you start The tutorial assumes that you already have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). Your application also needs you to create the tenants it requires. View the [previous tutorial](/docs/authentication/enterprise/initial-setup) for more information on how to do this. ## Steps ### 1. Change CORS setting and `websiteDomain` :::warning no-title You have to [create tenants](/docs/authentication/enterprise/initial-setup) before you can complete this step. ::: #### 1.1 CORS setup In order for the browser to be able to make requests to the backend, the CORS setting on the backend needs to reflect the right set of allowed origins. For example, if you have `customer1.example.com` on the frontend, then the CORS setting on the backend should allow `customer1.example.com` as an allowed origin. You can specifically whitelist the set of frontend sub domains on the backend, or you can use a regex like `*.example.com`. #### 1.2 `websiteDomain` setup Set the `websiteDomain` to `window.location.origin` in the frontend SDK initialization step. On the backend, update the `websiteDomain` to be the main domain (`example.com` if your sub domains are `sub.example.com`). Then override the `sendEmail` functions to change the domain of the link dynamically based on the tenant ID supplied to the `sendEmail` function. See the Email Delivery section in the docs for how to override the `sendEmail` function. ### 2. Load login methods dynamically on the frontend based on the `tenantId` Modify the `SuperTokens.init` to do the following: 1. Set the `usesDynamicLoginMethods` to true. This tells the frontend SDK that the login page relies on the `tenantId` and to fetch the tenant configuration from the backend before showing any login UI. 2. Initialize the `Multitenancy` recipe and provide `getTenantId` configuration function. ```tsx SuperTokens.init({ appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, // highlight-start usesDynamicLoginMethods: true, // highlight-end recipeList: [ // Other recipes.. // highlight-start Multitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: async () => { // We treat the sub domain as the tenant ID return window.location.host.split('.')[0] } } } }, }) // highlight-end ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, // highlight-start usesDynamicLoginMethods: true, // highlight-end recipeList: [ // Other recipes... supertokensUISession.init(), // highlight-start supertokensUIMultitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: async () => { // We treat the sub domain as the tenant ID return window.location.host.split('.')[0] } } } }, }) // highlight-end ] }); ``` You can fetch the list of user's login methods based on their tenant ID (which you can derive from the current sub domain value) as shown below. ```tsx async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { const loginMethods = await Multitenancy.getLoginMethods({ tenantId }) if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button } else { // more checks for other providers } } else { // thirdparty login is disabled for the tenant } } ``` ```tsx async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { const loginMethods = await Multitenancy.getLoginMethods({ tenantId }) if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button } else { // more checks for other providers } } else { // thirdparty login is disabled for the tenant } } ``` ```bash curl --location --request GET '/loginmethods' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: The `recipes` field contains information about which login methods are active along with the list of third party providers configured for this tenant. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. You also need to initialize the multi tenancy recipe with the following callback defined. You can get the value of `tenantId` from the sub domain as shown below. ```tsx SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", }, recipeList: [ // highlight-start Multitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: async () => { // We treat the sub domain as the tenant ID return window.location.host.split('.')[0] } } } }, }) // highlight-end // other recipes... ] }); ``` ```tsx supertokens.init({ appInfo: { appName: "...", apiDomain: "...", }, recipeList: [ // highlight-start supertokensMultitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: async () => { // We treat the sub domain as the tenant ID return window.location.host.split('.')[0] } } } }, }) // highlight-end // other recipes... ] }); ``` After you have shown the login methods and the user tries to sign in, follow all the steps for mobile app login similar to the [social login steps](../../custom-ui/thirdparty-login). When calling the sign in up API, also pass in the `tenantId` in the request path. An example of this appears below: ```bash curl --location --request POST '/signinup' \ --header 'Content-Type: application/json' \ --data-raw '{ "thirdPartyId": "...", "clientType": "...", "oAuthTokens": { "access_token": "...", "id_token": "..." }, }' ``` ### 3. Restrict subdomain access Restrict which sub domains the user has access to. To do this configure the SDK to know which domain each `tenantId` has access to. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ // highlight-start Multitenancy.init({ getAllowedDomainsForTenantId: async (tenantId, userContext) => { // query your db to get the allowed domain for the input tenantId // or you can make the tenantId equal to the sub domain itself return [tenantId + ".myapp.com", "myapp.com", "www.myapp.com"] } }), // highlight-end // other recipes... ] }) ``` ```go ```tsx Session.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await Session.getClaimValue({ claim: AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }) ``` Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever you check protect a route](/docs/additional-verification/session-verification/protect-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to their right sub domain (via the values set in the `AllowedDomainsClaim` session claim). Make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUISession.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await supertokensUISession.getClaimValue({ claim: supertokensMultitenancy.AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }) ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx Session.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await Session.getClaimValue({ claim: AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }) ``` Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever you check protect a route](../sessions/protecting-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to their right sub domain (via the values set in the `AllowedDomainsClaim` session claim). ```tsx Session.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await Session.getClaimValue({ claim: AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }) ``` ```tsx supertokensSession.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await supertokensSession.getClaimValue({ claim: supertokensMultitenancy.AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }) ``` Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever you check protect a route](../sessions/protecting-frontend-routes#verifying-the-claims-of-a-session--cust), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to their right sub domain (via the values set in the `AllowedDomainsClaim` session claim). --- # Authentication - Enterprise Login - Manage tenants Source: https://supertokens.com/docs/authentication/enterprise/manage-tenants ## Create a new tenant ```tsx async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"] }); if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } } ``` The snippet creates a new tenant with the id `"customer1"`. It enables the email password, third party and passwordless login methods for this tenant. You can also disable any of these by not including them in the `firstFactors` input. If `firstFactors` is not specified, by default, the system does not enable any of the login methods. If you set `firstFactors` to `null` the SDK uses any of the login methods. The built-in Factor IDs available for `firstFactors` include: | Authentication Type | Factor ID | |-------------------|-----------| | Email password auth | `emailpassword` | | Social login / enterprise SSO auth | `thirdparty` | | Passwordless - Email OTP | `otp-email` | | Passwordless - SMS OTP | `otp-phone` | | Passwordless - Email magic link | `link-email` | | Passwordless - SMS magic link | `link-phone` | ```go --- ## Update a tenant You can also configure a tenant to have different configurations per the core's `config.yaml` or docker environment variables. Below is how you can specify the configuration, when creating or modifying a tenant: ```tsx async function createNewTenant() { // highlight-start let resp = await Multitenancy.createOrUpdateTenant("customer1", { coreConfig: { "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2", } }); // highlight-end if (resp.createdNew) { // new tenant was created } else { // existing tenant's config was modified. } } ``` In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. ```go Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate async def some_func(): tenant_id = "customer1" result = await create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate( core_config={ "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2", }, )) if result.status != "OK": print("handle error") elif result.created_new: print("new tenant created") else: print("existing tenant's config was modified.") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate tenant_id = "customer1" result = create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate( core_config={ "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2", }, )) if result.status != "OK": print("handle error") elif result.created_new: print("new tenant created") else: print("existing tenant's config was modified.") ``` In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. ```bash curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data-raw '{ "tenantId": "customer1", "coreConfig": { "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2" } }' ``` In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. Custom tenant configuration In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. You can edit the values by clicking on the pencil icon and then specifying a new value. :::caution You cannot edit database connection settings directly from the Dashboard, and you may need to use the SDK or cURL to update them. ::: --- ## Get tenant details Once you have set the configs for a specific tenant, you can fetch the tenant info as shown below: ```tsx async function getTenant(tenantId: string) { // highlight-start let resp = await Multitenancy.getTenant(tenantId); // highlight-end if (resp === undefined) { // tenant does not exist } else { let coreConfig = resp.coreConfig; let firstFactors = resp.firstFactors; let configuredThirdPartyProviders = resp.thirdParty.providers; } } ``` ```go isThirdPartyLoginEnabled := tenant.ThirdParty.Enabled; isPasswordlessLoginEnabled := tenant.Passwordless.Enabled; if (isEmailPasswordLoginEnabled) { // Tenant support email password login } if (isThirdPartyLoginEnabled) { // Tenant support third party login configuredThirdPartyProviders := tenant.ThirdParty.Providers; fmt.Println(configuredThirdPartyProviders); } if (isPasswordlessLoginEnabled) { // Tenant support passwordless login } } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import get_tenant async def some_func(): tenant = await get_tenant("customer1") if tenant is None: print("tenant does not exist") else: core_config = tenant.core_config first_factors = tenant.first_factors providers = tenant.third_party_providers print(core_config) print(first_factors) print(providers) ``` ```python from supertokens_python.recipe.multitenancy.syncio import get_tenant tenant = get_tenant("customer1") if tenant is None: print("tenant does not exist") else: core_config = tenant.core_config first_factors = tenant.first_factors providers = tenant.third_party_providers print(core_config) print(first_factors) print(providers) ``` ```bash curl --location --request GET 'http://localhost:3567/customer1/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' ``` Notice that you add `customer1` to the path of the request. This tells the core that the tenant you want to get the information about is `customer1` (the one created before in this page). If the input tenant does not exist, you get back a `200` status code with the following JSON: ```json {"status": "TENANT_NOT_FOUND_ERROR"} ``` Otherwise you get a `200` status code with the following JSON output: ```json { "status": "OK", "thirdParty": { "providers": [...] }, "coreConfig": { "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2" }, "tenantId": "customer1", "firstFactors": ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-email", "link-phone"] } ``` The returned `coreConfig` is the same as what you set when creating / updating the tenant. The rest of the core configurations for this tenant inherit from the app's (or the `public` tenant) configuration. The `public` tenant, for the `public` app inherits its configurations from the `config.yaml` / docker environment variables values. --- ## List all the tenants of an app ```tsx async function listAllTenants() { // highlight-start let resp = await Multitenancy.listAllTenants(); let tenants = resp.tenants; // highlight-end tenants.forEach(tenant => { let coreConfig = tenant.coreConfig; let firstFactors = tenant.firstFactors; let configuredThirdPartyProviders = tenant.thirdParty.providers; }); } ``` The value of `firstFactors` can be as follows: - `undefined`: The core enables all login methods, and any auth recipe initialized in the backend SDK works. - `[]` (empty array): The tenant does not enable any login methods. - a non-empty array: The tenant enables only the login methods in the array. ```go currTenant := resp.OK.Tenants[i] coreConfig := currTenant.CoreConfig; fmt.Println(coreConfig) isEmailPasswordLoginEnabled := currTenant.EmailPassword.Enabled; isThirdPartyLoginEnabled := currTenant.ThirdParty.Enabled; isPasswordlessLoginEnabled := currTenant.Passwordless.Enabled; configuredThirdPartyProviders := currTenant.ThirdParty.Providers; if isEmailPasswordLoginEnabled { // Tenant has email password login enabled } if isThirdPartyLoginEnabled { // Tenant has third party login enabled fmt.Println(configuredThirdPartyProviders) } if isPasswordlessLoginEnabled { // Tenant has passwordless login enabled } } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import list_all_tenants async def some_func(): response = await list_all_tenants() if response.status != "OK": print("Handle error") return for tenant in response.tenants: core_configuration = tenant.core_config first_factors = tenant.first_factors configured_third_party_providers = tenant.third_party_providers print(core_configuration) print(f"First factors: {first_factors}") print(f"Configured third party providers: {configured_third_party_providers}") ``` ```python from supertokens_python.recipe.multitenancy.syncio import list_all_tenants def some_func(): response = list_all_tenants() if response.status != "OK": print("Handle error") return for tenant in response.tenants: core_config = tenant.core_config first_factors = tenant.first_factors configured_third_party_providers = tenant.third_party_providers print(core_config) print(f"First factors: {first_factors}") print(f"Configured third party providers: {configured_third_party_providers}") ``` ```bash curl --location --request GET '/recipe/multitenancy/tenant/list/v2' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json' ``` You get the following JSON output: ```json { "status": "OK", "tenants": [{ "tenantId": "customer1", "thirdParty": { "providers": [...] }, "coreConfig": {...}, "firstFactors": [...] }] } ``` The value of `firstFactors` can be as follows: - `undefined`: The core enables all login methods, and any auth recipe initialized in the backend SDK works. - `[]` (empty array): The tenant does not enable any login methods. - a non-empty array: The tenant enables only the login methods in the array. --- ## Add a custom third-party provider to a tenant If you can't find a provider in [the built-in list](/docs/authentication/social/built-in-providers-config), you can add your own custom implementation. This page shows you how to do that on a per tenant basis. :::info Note If you think that SuperTokens should support this provider by default, make sure to let the team know [on GitHub](https://github.com/supertokens/supertokens-node/issues/88). ::: Once you have created a tenant, you want to call the API / function to create a new provider for the tenant as shown below. ### Using OAuth endpoints ```tsx async function createTenant() { let resp = await Multiteancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "custom", name: "Custom Provider", clients: [{ clientId: "...", clientSecret: "...", scope: ["email", "profile"] }], authorizationEndpoint: "https://example.com/oauth/authorize", authorizationEndpointQueryParams: { // optional "someKey1": "value1", "someKey2": null, }, tokenEndpoint: "https://example.com/oauth/token", tokenEndpointBodyParams: { "someKey1": "value1", }, userInfoEndpoint: "https://example.com/oauth/userinfo", userInfoMap: { fromUserInfoAPI: { userId: "id", email: "email", emailVerified: "email_verified", } } }); if (resp.createdNew) { // custom provider added to tenant } else { // existing custom provider config overwritten for tenant } } ``` ```go UserId: "id", Email: "email", EmailVerified: "email_verified", }, }, }, nil) // highlight-end if err != nil { // handle error } if resp.OK.CreatedNew { // Custom provider added to tenant } else { // Existing custom provider config overwritten for tenant } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig, UserInfoMap, UserFields async def some_func(): tenant_id = "..." result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="custom", name="Custom Provider", clients=[ ProviderClientConfig( client_id="...", client_secret="...", scope=["email", "profile"], ), ], authorization_endpoint="https://example.com/oauth/authorize", authorization_endpoint_query_params={ "someKey1": "value1", "someKey2": None, }, token_endpoint="https://example.com/oauth/token", token_endpoint_body_params={ "someKey1": "value1", }, user_info_endpoint="https://example.com/oauth/userinfo", user_info_map=UserInfoMap( from_user_info_api=UserFields( user_id="id", email="email", email_verified="email_verified", ), from_id_token_payload=UserFields(), ), )) if result.status != "OK": print("handle error") elif result.created_new: print("Custom provider added to tenant") else: print("Existing custom provider config overwritten for tenant") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig, UserInfoMap, UserFields tenant_id = "..." result = create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="custom", name="Custom Provider", clients=[ ProviderClientConfig( client_id="...", client_secret="...", scope=["email", "profile"], ), ], authorization_endpoint="https://example.com/oauth/authorize", authorization_endpoint_query_params={ "someKey1": "value1", "someKey2": None, }, token_endpoint="https://example.com/oauth/token", token_endpoint_body_params={ "someKey1": "value1", }, user_info_endpoint="https://example.com/oauth/userinfo", user_info_map=UserInfoMap( from_user_info_api=UserFields( user_id="id", email="email", email_verified="email_verified", ), from_id_token_payload=UserFields(), ), )) if result.status != "OK": print("handle error") elif result.created_new: print("Custom provider added to tenant") else: print("Existing custom provider config overwritten for tenant") ``` ```bash curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "custom", "name": "Custom provider", "clients": [{ "clientId": "...", "clientSecret": "...", "scope": ["email", "profile"] }], "authorizationEndpoint": "https://example.com/oauth/authorize", "authorizationEndpointQueryParams": { "someKey1": "value1", "someKey2": "value2" }, "tokenEndpoint": "https://example.com/oauth/token", "tokenEndpointBodyParams": { "someKey1": "value1" }, "userInfoEndpoint": "https://example.com/oauth/userinfo", "userInfoMap": { "fromUserInfoAPI": { "userId": "id", "email": "email", "emailVerified": "email_verified" } } } }' ``` Click on **Add new provider** in the Social/Enterprise Providers section Social/Enterprise providers Select **Add Custom Provider** option New Provider Fill in the details as shown below and click on **Save** OAuth2 provider You can see all the options in the [CDI documentation](https://supertokens.com/docs/cdi). | Field | Description | Example | |-------|-------------|---------| | `tenantId` | Unique ID that identifies the tenant. If not specified, defaults to `"public"` | `"customer1"` | | `thirdPartyId` | Unique ID for identifying the provider | `"google"` | | `name` | Display name used for the login button UI | `"XYZ"` → displays "Login using XYZ" | | `clients` | Array of client credentials/settings. Can contain multiple items for different client types (web/mobile) | Contains `clientId`, `clientSecret`, and optional `clientType` | | `authorizationEndpoint` | URL for user login | `"https://accounts.google.com/o/oauth2/v2/auth"` | | `authorizationEndpointQueryParams` | Optional configuration to modify query params | | | `tokenEndpoint` | API endpoint for exchanging Authorization Code | `"https://oauth2.googleapis.com/token"` | | `tokenEndpointBodyParams` | Optional configuration to modify request body | | | `userInfoEndpoint` | API endpoint that provides user information | `"https://www.googleapis.com/oauth2/v1/userinfo"` | | `userInfoMap` | Maps provider's JSON response to user info fields. Use dot notation to map nested fields: `user.id` | ```{ userId: "id", email: "email", emailVerified: "email_verified" }``` | ### Using OpenID Connect endpoints If the provider is Open ID Connect (OIDC) compatible, you can provide a URL for the `OIDCDiscoverEndpoint` configuration. The backend SDK automatically discovers authorization endpoint, token endpoint and the user info endpoint by querying the `/.well-known/openid-configuration`. ```tsx async function createTenant() { let resp = await Multiteancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "custom", name: "Custom Provider", clients: [{ clientId: "...", clientSecret: "...", scope: ["email", "profile"] }], // highlight-start oidcDiscoveryEndpoint: "https://example.com/.well-known/openid-configuration", // highlight-end authorizationEndpointQueryParams: { // optional "someKey1": "value1", "someKey2": null, }, userInfoMap: { fromIdTokenPayload: { userId: "id", email: "email", emailVerified: "email_verified", } } }); if (resp.createdNew) { // custom provider added to tenant } else { // existing custom provider config overwritten for tenant } } ``` ```go UserId: "id", Email: "email", EmailVerified: "email_verified", }, }, }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Custom provider added to tenant } else { // Existing custom provider config overwritten for tenant } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig, UserInfoMap, UserFields async def some_func(): tenant_id = "..." result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="custom", name="Custom Provider", clients=[ ProviderClientConfig( client_id="...", client_secret="...", scope=["email", "profile"], ), ], # highlight-start oidc_discovery_endpoint="https://example.com/.well-known/openid-configuration", # highlight-end authorization_endpoint_query_params={ "someKey1": "value1", "someKey2": None, }, user_info_map=UserInfoMap( from_user_info_api=UserFields(), from_id_token_payload=UserFields( user_id="id", email="email", email_verified="email_verified", ), ), )) if result.status != "OK": print("handle error") elif result.created_new: print("Custom provider added to tenant") else: print("Existing custom provider config overwritten for tenant") ``` ```bash curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "custom", "name": "Custom provider", "clients": [{ "clientId": "...", "clientSecret": "...", "scope": ["email", "profile"] }], "oidcDiscoveryEndpoint": "https://example.com/.well-known/openid-configuration", "authorizationEndpointQueryParams": { "someKey1": "value1", "someKey2": "value2" }, "userInfoMap": { "fromIdTokenPayload": { "userId": "id", "email": "email", "emailVerified": "email_verified" } } } }' ``` Click on **Add new provider** in the Social/Enterprise Providers section Social/Enterprise providers Select **Add Custom Provider** option New Provider Fill in the details as shown below and click on **Save** OAuth2 provider You can see all the options in the [CDI documentation](https://supertokens.com/docs/cdi). | Field | Description | |-------|-------------| | `tenantId` | Unique ID that identifies the tenant. If not specified, defaults to `"public"` | | `thirdPartyId`, `name`, `clients` | Configuration values similar to OAuth endpoints method | | `userInfoMap.fromIdTokenPayload` | Maps user info from the ID token payload | | `userInfoMap.fromUserInfoAPI` | Optional mapping from user info API. You can combine it with ID token payload mapping | --- ## Add a user to a tenant When a user creates an account, they receive a `tenantId` to sign up. This means that the user can only log in to that tenant. SuperTokens allows you to assign a user ID to multiple tenants. This is possible as long as that user's email or phone number is unique for that login method, for each of the new tenants. Once associated with multiple tenants, that user can log in to each of the tenants they have access to. For example, if a user signs up with email password login in the `public` tenant with email `user@example.com`, they can join another tenant (`t1` for example). This is possible as long as `t1` does not already have an email password user with the same email (that is `user@example.com`). To associate a user with a tenant, you can call the following API: ```tsx async function addUserToTenant(recipeUserId: RecipeUserId, tenantId: string) { let resp = await Multitenancy.associateUserToTenant(tenantId, recipeUserId); if (resp.status === "OK") { // User is now associated with tenant } else if (resp.status === "UNKNOWN_USER_ID_ERROR") { // The provided user ID was not one that signed up using one of SuperTokens' auth recipes. } else if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { // This means that the input user is one of passwordless or email password logins, and the new tenant already has a user with the same email for that login method. } else if (resp.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR") { // This means that the input user is a passwordless user and the new tenant already has a user with the same phone number, for passwordless login. } else if (resp.status === "ASSOCIATION_NOT_ALLOWED_ERROR") { // This can happen if using account linking along with multi tenancy. One example of when this // happens if if the target tenant has a primary user with the same email / phone numbers // as the current user. } else { // status is THIRD_PARTY_USER_ALREADY_EXISTS_ERROR // This means that the input user had already previously signed in with the same third party provider (e.g. Google) for the new tenant. } } ``` ```go You can even remove a user's access from a tenant using the API call shown below. In fact, you can remove a user from all tenants that they have access to, and the user and their metadata remain in the system. However, they cannot log in to any tenant. To remove a user from a tenant, call the following API: ```tsx async function removeUserFromTeannt(recipeUserId: RecipeUserId, tenantId: string) { let resp = await Multitenancy.disassociateUserFromTenant(tenantId, recipeUserId); if (resp.wasAssociated) { // User was removed from tenant } else { // User was never a part of the tenant anyway } } ``` ```go } else { // User was never a part of the tenant anyway } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import disassociate_user_from_tenant from supertokens_python.types import RecipeUserId async def some_func(): response = await disassociate_user_from_tenant("customer1", RecipeUserId("user1")) if response.was_associated: print("User was removed from tenant") else: print("User was never a part of the tenant anyway") ``` ```python from supertokens_python.recipe.multitenancy.syncio import disassociate_user_from_tenant from supertokens_python.types import RecipeUserId def some_func(): response = disassociate_user_from_tenant("customer1", RecipeUserId("user1")) if response.was_associated: print("User was removed from tenant") else: print("User was never a part of the tenant anyway") ``` ```bash curl --location --request POST '//recipe/multitenancy/tenant/user/remove \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json' \ --data-raw '{ "recipeUserId": "..." }' ``` :::important - Users can only share access across tenants and not across apps. - If your app has two tenants, that are in different database locations, then you cannot share users between them. ::: # Authentication - Enterprise Login - Manage apps Source: https://supertokens.com/docs/authentication/enterprise/manage-apps ## Run multiple apps using the same SuperTokens core Like how you can create multiple tenants / user pools within one SuperTokens core, you can create multiple apps within one core as well: - Each app operates in isolation and can have multiple tenants. - Each app can have its own database or share a database with another app (and yet remain logically isolated). - Each app can have its own set of [core and db configurations](https://github.com/supertokens/supertokens-core/blob/master/config.yaml). If a specific configuration is not explicitly set for an app, it inherits from the base configuration.yaml / docker environment variables configuration. - The core and db configurations of each tenant within an app inherit from the configurations of that app. You can use this feature to deploy one SuperTokens core across multiple independent apps within your company. Additionally, you can create multiple development environments (`dev`, staging, prod, etc.) for one app without deploying individual SuperTokens core instances. ### 1. Create a new app in the core :::caution This is a paid feature, even if creating an additional `dev` `env` on the managed service, or if using the `dev` license keys in case of self-hosting. The pricing is $50 / month / additional app. Please reach out to [support@SuperTokens.com](mailto:support@SuperTokens.com) if you have any questions, or if you want to create multiple `environments` and want a bulk discount. ::: To create a new app in the SuperTokens core, you can use the following cURL command: ```bash curl --location --request PUT '/recipe/multitenancy/app/v2' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json' \ --data-raw '{ "appId": "app1", "coreConfig": {...} }' ``` - The above command creates (or updates) an app with the `appId` of `app1`. - It also creates a default tenant for this app with the tenant ID of `public` (that is, the default `tenantId`). - You can set core configurations for this app (see the configuration.yaml / docker environment variable options for your core). The core configurations for a new app inherit from the configurations provided in the configuration.yaml / docker environment (or the edit configuration dashboard for managed service). - By default, all the login methods enable for a new app (specifically, the `public` tenant of the new app), but you can pass in `firstFactors` input to specifically enable selected login methods. The built-in Factor IDs that you can use for `firstFactors` are: - Email password auth: `emailpassword` - Social login / enterprise SSO auth: `thirdparty` - Passwordless: - With email OTP: `otp-email` - With SMS OTP: `otp-phone` - With email magic link: `link-email` - With SMS magic link: `link-phone` ```bash curl --location --request PUT '/recipe/multitenancy/app' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json' \ --data-raw '{ "appId": "app1", "thirdPartyEnabled": true, "passwordlessEnabled": true, "emailPasswordEnabled": true, "coreConfig": {...} }' ``` - The above command creates (or updates) an app with the `appId` of `app1`. - It also creates a default tenant for this app with the tenant ID of `public` (that is, the default `tenantId`). - You can set core configurations for this app (see the configuration.yaml / docker environment variable options for your core). The core configurations for a new app inherit from the configurations provided in the configuration.yaml / docker environment (or the edit configuration dashboard for managed service). - By default, all the login methods enable for a new app (specifically, the `public` tenant of the new app), but you can pass in `false` to any of the login methods specified above to disable them. :::important Even if a login method enables for a tenant, you still require to initialize the right recipe on the backend for sign up / in to be possible with that login method. For example, if for a tenant, you have enabled the passwordless login method, but don't use the passwordless (or a combination recipe that has passwordless) on the backend, then end users cannot sign up / in using the passwordless APIs because those APIs are not exposed via the backend SDK's middleware. ::: ### 2. Configure the `appId` during backend SDK init Whilst one core can have multiple apps, you must use a dedicated backend (integrated with the backend SDK) per app. For example, if you have two apps, and both use a NodeJS backend, then you need to configure one app's backend to have `appId` as `app1` (as an example). The other app's backend should have `appId` as `app2`. You can specify an `appId` on the backend SDK SuperTokens.init by appending the `appId` to the `connectionUri` as shown below: ```tsx supertokens.init({ supertokens: { // highlight-next-line connectionURI: "http://localhost:3567/appid-app1", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [] }); ``` ```go The following snippet shows you how to delete an app from a SuperTokens Core instance. This operation is irreversible and deletes all user data associated with the app. :::important Before you delete an app, ensure that you satisfy the following requirements: - The request must originate from the public app and tenant - The app must not have any tenants other than the public tenant. You need to delete other tenants first. After deleting an app, make sure to update any backend services configured to use this app ID to prevent unexpected errors. ::: ```bash curl --location --request POST '/recipe/multitenancy/app/remove' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json' \ --data-raw '{ "appId": "app1" }' ``` - The above command deletes the app with the `appId` of `app1` and all its associated tenants. - All user data, configuration, and tenant information associated with this app are permanently deleted. - The API key used must have the necessary permissions to delete apps. :::danger This operation cannot be undone. Make sure you have backed up any important data before proceeding. ::: # Authentication - Enterprise Login - SAML - Integration guide Source: https://supertokens.com/docs/authentication/enterprise/saml/boxy-hq-guide ## Before you start These instructions assume that you already are familiar with **SuperTokens** and you have configured a demo application. If you have skipped those steps page please go through the main [quickstart guide](/docs/quickstart/introduction). ## Using the SuperTokens dashboard :::caution This is only available with Node and Python SDKs. ::: ### 1. Generate the XML metadata file from your SAML provider Your SAML provider allows you to download a `.xml` file that you can upload to SAML Jackson. During this process, you need to provide it: - the SSO URL and; - the Entity ID. You can learn more about these in the [SAML Jackson docs](https://boxyhq.com/docs/jackson/configure-saml-idp). In the example app, [mocksaml.com](https://mocksaml.com/) serves as a free SAML provider for testing. When you navigate to the site, you see a "Download metadata" button which you should click on to get the `.xml` file. ### 2. Start the SAML Jackson service You can run SAML Jackson [with or without Docker](https://boxyhq.com/docs/jackson/deploy/service). ```bash docker run \ -p 5225:5225 \ -e JACKSON_API_KEYS="secret" \ -e DB_ENGINE="sql" \ -e DB_TYPE="postgres" \ -e DB_URL="postgres://postgres:postgres@postgres:5432/postgres" \ -d boxyhq/jackson ``` This starts the SAML Jackson server on `http://localhost:5225`. :::important If you are using the SuperTokens managed service, Boxy HQ hosts the server for you ([contact support](mailto:support@supertokens.com) to activate your instance). ::: ### 3. Create a new tenant in SuperTokens (if not done already) Create Tenant ### 4. Configure the SAML provider for the tenant Create Tenant To configure SAML login with SuperTokens, ensure that you use the correct provider name in the third-party configuration. Make sure to specify provider name with one of the following: - Microsoft Entra ID
- Microsoft AD FS
- Okta
- Auth0
- Google
- OneLogin
- PingOne
- JumpCloud
- Rippling
- SAML
Make sure to replace `http://localhost:5225` with the correct value for where you have hosted the BoxyHQ server. If you are using the SuperTokens managed service, Boxy HQ hosts the server for you ([contact support](mailto:support@supertokens.com) to activate your instance). :::success You have successfully configured a new tenant in SuperTokens. The next step is to wire up the frontend SDK to show the right login UI for this tenant. The specifics of this step depend on the UX that you want to provide to your users. The "Common UX flows" section documents two common UX flows. ::: ### 5. Adding multiple SAML connections to a single tenant If you have one SAML connection for a tenant, then the `Third Party Id` for that connection can be `boxy-saml`. This displays a single "SAML Login" button on the pre-built UI. If you want to add a second SAML connection for the same tenant, follow the same steps as above, but also use the `Add Suffix` option for the Third Party Id. For example, if a tenant has Active Directory and Okta login (both with SAML), you can create the Active Directory provider using `"boxy-saml"` as the `Third Party Id`. For Okta, you could use `okta` as a suffix to make the `Third Party Id` equal to `"boxy-saml-okta"`. You can also give them different names. Instead of "SAML Login" (that's shown above), you can use "Active Directory" and "Okta" to ensure that the button on the pre-built UI shows the right name. --- ## Using the BoxyHQ API ### 1. Generate the XML metadata file from your SAML provider Your SAML provider allows you to download a `.xml` file that you can upload to SAML Jackson. During this process, you need to provide it: - the SSO URL and; - the Entity ID. You can learn more about these in the [SAML Jackson docs](https://boxyhq.com/docs/jackson/configure-saml-idp). In the example app, [mocksaml.com](https://mocksaml.com/) serves as a free SAML provider for testing. When you navigate to the site, you see a "Download metadata" button which you should click on to get the `.xml` file. ### 2. Convert the `.xml` file to base64 You can use [an online base64 encoder](https://www.base64encode.org/) to do this. First copy the contents of the `.xml` file, and then put it in the encoder tool. The output string is the base64 version of the .xml file. For example, with an input `.xml` file (obtained from mocksaml.com): ```text MIIC4jCCAcoCCQC33wnybT5QZDANBgkqhkiG9w0BAQsFADAyMQswCQYDVQQGEwJV SzEPMA0GA1UECgwGQm94eUhRMRIwEAYDVQQDDAlNb2NrIFNBTUwwIBcNMjIwMjI4 MjE0NjM4WhgPMzAyMTA3MDEyMTQ2MzhaMDIxCzAJBgNVBAYTAlVLMQ8wDQYDVQQK DAZCb3h5SFExEjAQBgNVBAMMCU1vY2sgU0FNTDCCASIwDQYJKoZIhvcNAQEBBQAD ggEPADCCAQoCggEBALGfYettMsct1T6tVUwTudNJH5Pnb9GGnkXi9Zw/e6x45DD0 RuRONbFlJ2T4RjAE/uG+AjXxXQ8o2SZfb9+GgmCHuTJFNgHoZ1nFVXCmb/Hg8Hpd 4vOAGXndixaReOiq3EH5XvpMjMkJ3+8+9VYMzMZOjkgQtAqO36eAFFfNKX7dTj3V pwLkvz6/KFCq8OAwY+AUi4eZm5J57D31GzjHwfjH9WTeX0MyndmnNB1qV75qQR3b 2/W5sGHRv+9AarggJkF+ptUkXoLtVA51wcfYm6hILptpde5FQC8RWY1YrswBWAEZ NfyrR4JeSweElNHg4NVOs4TwGjOPwWGqzTfgTlECAwEAATANBgkqhkiG9w0BAQsF AAOCAQEAAYRlYflSXAWoZpFfwNiCQVE5d9zZ0DPzNdWhAybXcTyMf0z5mDf6FWBW 5Gyoi9u3EMEDnzLcJNkwJAAc39Apa4I2/tml+Jy29dk8bTyX6m93ngmCgdLh5Za4 khuU3AM3L63g7VexCuO7kwkjh/+LqdcIXsVGO6XDfu2QOs1Xpe9zIzLpwm/RNYeX UjbSj5ce/jekpAw7qyVVL4xOyh8AtUW1ek3wIw1MJvEgEPt0d16oshWJpoS1OT8L r/22SvYEo3EmSGdTVGgk3x3s+A0qWAqTcyjr7Q4s/GKYRFfomGwz0TZ4Iw1ZN99M m0eo2USlSRTVl7QHRTuiuSThHpLKQQ== urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress ``` The base64 output is: ```text PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PEVudGl0eURlc2NyaXB0b3IgeG1sbnM6bWQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDptZXRhZGF0YSIgZW50aXR5SUQ9Imh0dHBzOi8vc2FtbC5leGFtcGxlLmNvbS9lbnRpdHlpZCIgdmFsaWRVbnRpbD0iMjAyNi0wNi0yMlQxODozOTo1My4wMDBaIj48SURQU1NPRGVzY3JpcHRvciBXYW50QXV0aG5SZXF1ZXN0c1NpZ25lZD0iZmFsc2UiIHByb3RvY29sU3VwcG9ydEVudW1lcmF0aW9uPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiPjxLZXlEZXNjcmlwdG9yIHVzZT0ic2lnbmluZyI+PEtleUluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxYNTA5RGF0YT48WDUwOUNlcnRpZmljYXRlPk1JSUM0akNDQWNvQ0NRQzMzd255YlQ1UVpEQU5CZ2txaGtpRzl3MEJBUXNGQURBeU1Rc3dDUVlEVlFRR0V3SlYKU3pFUE1BMEdBMVVFQ2d3R1FtOTRlVWhSTVJJd0VBWURWUVFEREFsTmIyTnJJRk5CVFV3d0lCY05Nakl3TWpJNApNakUwTmpNNFdoZ1BNekF5TVRBM01ERXlNVFEyTXpoYU1ESXhDekFKQmdOVkJBWVRBbFZMTVE4d0RRWURWUVFLCkRBWkNiM2g1U0ZFeEVqQVFCZ05WQkFNTUNVMXZZMnNnVTBGTlREQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQUQKZ2dFUEFEQ0NBUW9DZ2dFQkFMR2ZZZXR0TXNjdDFUNnRWVXdUdWROSkg1UG5iOUdHbmtYaTlady9lNng0NUREMApSdVJPTmJGbEoyVDRSakFFL3VHK0FqWHhYUThvMlNaZmI5K0dnbUNIdVRKRk5nSG9aMW5GVlhDbWIvSGc4SHBkCjR2T0FHWG5kaXhhUmVPaXEzRUg1WHZwTWpNa0ozKzgrOVZZTXpNWk9qa2dRdEFxTzM2ZUFGRmZOS1g3ZFRqM1YKcHdMa3Z6Ni9LRkNxOE9Bd1krQVVpNGVabTVKNTdEMzFHempId2ZqSDlXVGVYME15bmRtbk5CMXFWNzVxUVIzYgoyL1c1c0dIUnYrOUFhcmdnSmtGK3B0VWtYb0x0VkE1MXdjZlltNmhJTHB0cGRlNUZRQzhSV1kxWXJzd0JXQUVaCk5meXJSNEplU3dlRWxOSGc0TlZPczRUd0dqT1B3V0dxelRmZ1RsRUNBd0VBQVRBTkJna3Foa2lHOXcwQkFRc0YKQUFPQ0FRRUFBWVJsWWZsU1hBV29acEZmd05pQ1FWRTVkOXpaMERQek5kV2hBeWJYY1R5TWYwejVtRGY2RldCVwo1R3lvaTl1M0VNRURuekxjSk5rd0pBQWMzOUFwYTRJMi90bWwrSnkyOWRrOGJUeVg2bTkzbmdtQ2dkTGg1WmE0CmtodVUzQU0zTDYzZzdWZXhDdU83a3dramgvK0xxZGNJWHNWR082WERmdTJRT3MxWHBlOXpJekxwd20vUk5ZZVgKVWpiU2o1Y2UvamVrcEF3N3F5VlZMNHhPeWg4QXRVVzFlazN3SXcxTUp2RWdFUHQwZDE2b3NoV0pwb1MxT1Q4TApyLzIyU3ZZRW8zRW1TR2RUVkdnazN4M3MrQTBxV0FxVGN5anI3UTRzL0dLWVJGZm9tR3d6MFRaNEl3MVpOOTlNCm0wZW8yVVNsU1JUVmw3UUhSVHVpdVNUaEhwTEtRUT09CjwvWDUwOUNlcnRpZmljYXRlPjwvWDUwOURhdGE+PC9LZXlJbmZvPjwvS2V5RGVzY3JpcHRvcj48TmFtZUlERm9ybWF0PnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzczwvTmFtZUlERm9ybWF0PjxTaW5nbGVTaWduT25TZXJ2aWNlIEJpbmRpbmc9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpiaW5kaW5nczpIVFRQLVJlZGlyZWN0IiBMb2NhdGlvbj0iaHR0cHM6Ly9tb2Nrc2FtbC5jb20vYXBpL3NhbWwvc3NvIi8+PFNpbmdsZVNpZ25PblNlcnZpY2UgQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCIgTG9jYXRpb249Imh0dHBzOi8vbW9ja3NhbWwuY29tL2FwaS9zYW1sL3NzbyIvPjwvSURQU1NPRGVzY3JpcHRvcj48L0VudGl0eURlc2NyaXB0b3I+ ``` ### 3. Start the SAML Jackson service You can run SAML Jackson [with or without Docker](https://boxyhq.com/docs/jackson/deploy/service). ```bash docker run \ -p 5225:5225 \ -e JACKSON_API_KEYS="secret" \ -e DB_ENGINE="sql" \ -e DB_TYPE="postgres" \ -e DB_URL="postgres://postgres:postgres@postgres:5432/postgres" \ -d boxyhq/jackson ``` This starts the SAML Jackson server on `http://localhost:5225`. :::important If you are using the SuperTokens managed service, Boxy HQ hosts the server for you ([contact support](mailto:support@supertokens.com) to activate your instance). ::: ### 4. Upload the base64 XML string to SAML Jackson Take the string generated in step (2) and run: ```bash curl --location --request POST 'http://localhost:5225/api/v1/saml/config' \ --header 'Authorization: Api-Key secret' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'encodedRawMetadata=' \ --data-urlencode 'defaultRedirectUrl=' \ --data-urlencode 'redirectUrl=[""]' \ --data-urlencode 'tenant=' \ --data-urlencode 'product=' \ --data-urlencode 'name=demo-config' \ --data-urlencode 'description=Demo SAML config' ``` You can learn more about the configuration values [in the SAML Jackson docs](https://boxyhq.com/docs/jackson/saml-flow#2-saml-config-api). For the example app, you can see [this command here](https://github.com/supertokens/jackson-supertokens-express/blob/main/addTenant.sh). This helper script, `addTenant.sh`, provides the command. You can run it like: ```bash ./addTenant.sh # example ./addTenant.sh customer1 ./addTenant.sh customer2 ``` The output of this command provides you the `client_id` and `client_secret` for this tenant. You need to provide these values to SuperTokens for this tenant when configuring this tenant's providers (see below). ### 5. Create a new tenant in SuperTokens (if not done already) ```tsx async function createTenant() { let resp = await Multiteancy.createOrUpdateTenant("customer1", { firstFactors: ["thirdparty"] }); if (resp.createdNew) { // new tenant was created } else { // existing tenant's config was modified. } } ``` ```go async function addThirdPartyConfigToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "boxy-saml", name: "", clients: [{ clientId: "", clientSecret: "", additionalConfig: { "boxyURL": "http://localhost:5225", } }] }); if (resp.createdNew) { // SAML Login added to customer1 } else { // Existing SAML Login config overwritten for customer1 } } ``` ```go # Authentication - Unified Login - OAuth2 basics Source: https://supertokens.com/docs/authentication/unified-login/oauth2-basics ## Overview Each quickstart guide uses OAuth2 specific concepts that you should be aware of. Read through this page to get a better understanding of the specifications. **OAuth2**, Open Authorization, is an industry-standard authorization framework that enables third-party applications to obtain limited access to a user's resources without exposing their credentials. ## Terminology ### Roles In OAuth, roles define the different responsibilities of entities involved in the process of granting and obtaining access to protected resources. The specification defines four roles: #### Resource Owner The **Resource Owner** is an entity capable of granting access to a protected resource. This is an actual person that uses an application. #### Client An **OAuth 2.0 Client** is an application that wants to access protected resources. It needs to get an [**OAuth2 Access Token**](#oauth2-access-token) from the [**Authorization Server**](#authorization-server). With that token the client can perform authorized operations on behalf of the [**Resource Owner**](#resource-owner). The term **client** does not imply any particular implementation characteristics (for example, whether the application executes on a server, a desktop, or other devices). #### Resource Server The server hosting the protected resources, capable of accepting and responding to protected resource requests using [**OAuth2 Access Tokens**](#oauth2-access-token). Some real-world examples in this case would be things like: - A file storage service that allows users to access only their files - A social media application that allows users to access only posts from their friends - A chat app that shows only messages from conversations in which the user is a participant #### Authorization Server The server issuing [**OAuth2 Access Tokens**](#oauth2-access-token) to the [**Client**](#client) after successfully authenticating the [**Resource Owner**](#resource-owner). ### Tokens Tokens are strings that represent the authorization issued to the [**Client**](#client). They are mainly used to access to protected resources, on behalf of the [**Resource Owner**](#resource-owner). At the same time, tokens can provide more information about who the owner is. #### OAuth2 Access Token This is the main token that provides temporary access to protected resources. The **OAuth2 Access Token** should only be accessed and validated by the [**Resource Server**](#resource-server). :::info This token is different from the **SuperTokens Session Access Token**. The latter functions in the **OAuth 2.0** authentication flows to maintain a session between the **authorization frontend** and the **authorization backend server**. ::: #### OAuth2 Refresh Token A token that allows obtaining a new [**OAuth2 Access Token**](#oauth2-access-token) when the current one has expired. Using the refresh token does not require the user to re-authenticate. :::info This token is different from the **SuperTokens Session Refresh Token**. The latter functions in the **OAuth 2.0** authentication flows to maintain a session between the **authorization frontend** and the **authorization backend server**. ::: #### ID Token This token provides identity information about the [**Resource Owner**](#resource-owner). Unlike [**OAuth2 Access Tokens**](#oauth2-access-token), the **ID Tokens** should only be accessed by the [**Client**](#client). ### Scopes Scopes define the range of access that the [**Client**](#client) is requesting on behalf of the [**Resource Owner**](#resource-owner). They specify what portions of the **Resource Owner’s** data the **Client** can access and what actions it can perform. For example, when a user grants a web application permission to read their email, the application might request the `email` scope. In a general authentication flow scopes get used in the following way: 1. When the **Client** gets created, it configures a series of scopes for the **Authorization Server** 2. The **Authorization Server** authenticates the **Resource Owner** and uses the scopes to generate an **OAuth2 Access Token**. 3. The **Resource Server** checks the scopes of the **OAuth2 Access Token** and only allows the requested actions. ### Authorization flows The **OAuth 2.0** protocol defines several *flows* to accommodate different use cases. They are a set of steps an **OAuth Client** has to perform to obtain an access token. Our implementation supports the following flow types: #### [Authorization Code Grant](https://oauth.net/2/grant-types/authorization-code/) Authorization Code Grant This flow is best suited for scenarios that involve **web applications**. It consists of the following steps: 1. The **Client** redirects the **Resource Owner** to the **Authorization Server’s** authorization endpoint. 2. If the **Resource Owner** grants permission, the **Authorization Server** redirects their browser back to the specified **Redirect URI** and includes an **Authorization Code** as a query parameter. 3. The **Client** then sends a request to the **Authorization Server**’s token endpoint, including the **Authorization Code**. 4. The **Authorization Server** verifies the information sent by the **Client** and, if valid, issues an **OAuth2 Access Token**. 5. The token can make requests to the **Resource Server** to access the protected resources on behalf of the **Resource Owner**. ##### Authorization code An **Authorization Code** is a short-lived code that the [**Authorization Server**](#authorization-server) provides to the [**Client**](#client), via a **Redirect URI**, after authorization approval. This code then gets exchanged for an [**OAuth2 Access Token**](#oauth2-access-token). The **Authorization Code** flow enhances security by keeping tokens out of the user-agent and letting the [**Client**](#client) manage the backend communication with the [**Authorization Server**](#authorization-server). ##### Proof key for code exchange (PKCE) To prevent cross-site request forgery (CSRF) and code injection attacks, the **Authorization Code flow** can use [**PKCE**](https://oauth.net/2/pkce/). At the beginning of the authentication flow the **Client** generates a random string called a *code verifier*. This ensures that, even if the **Authorization Code** gets intercepted, it cannot be exchanged for a token without also including the initial code. #### [Client credentials](https://oauth.net/2/grant-types/client-credentials/) Client Credentials Grant This flow is best suited for **machine-to-machine** (M2M) interactions where there is no end-user. It consists of the following steps: 1. The **Client** authenticates with the **Authorization Server** using its own credentials. 2. The **Authorization Server** verifies the credentials. 3. The **Authorization Server** returns an **OAuth2 Access Token**. 4. The **Client** uses the **OAuth2 Access Token** to access protected resources. 5. The **Resource Server** validates the **OAuth2 Access Token**. 6. If the validation is successful, the **Resource Server** returns the requested resources. # Authentication - Unified Login - Quickstart Guides - Multiple frontend domains with a common backend Source: https://supertokens.com/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-a-single-backend ## Overview You can implement the following guide if you have multiple **`frontend applications`** that use the same **`backend service`**. The authentication flow works in the following way: ## The User accesses the `frontend` application: - The application `frontend` redirects the user to the **Authorization Service** backend, using the authorize URL. - The **Authorization Service** backend redirects the user to the login UI. ## The User completes the login attempt: - The **Authorization Service** backend redirects the user to the `callback URL`. ## The user accesses the callback URL: - The `frontend` uses the callback URL information to obtain a **OAuth2 Access Token** from the **Authorization Service** backend. Multiple Frontend Domains with a Single Backend ## Before you start These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). If you have skipped that page, please follow the tutorial and return here once you're done. :::info If your frontend applications are on the same **domain**, but on different **sub-domains**, you can use [Session Sharing Across Subdomains](/docs/post-authentication/session-management/share-session-across-sub-domains). ::: ## Steps ### 1. Enable the Unified Login feature Go to the [**SuperTokens.com SaaS Dashboard**](https://supertokens.com) and follow these instructions: 1. Click on the **Enabled Paid Features** button 2. Click on **Managed Service** 3. Check the **Unified Login / M2M** option 4. Click *Save* ### 2. Create the OAuth2 Clients For each of your **`frontend`** applications create a separate [**OAuth2 client**](/docs/authentication/unified-login/oauth2-basics#client). This can occur by directly calling the **SuperTokens Core** API. ```bash curl --location --request POST '/recipe/oauth/clients' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data ' { "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "scope": "offline_access ", "redirectUris": ["https:///oauth/callback"], } ' ``` ```tsx const BASE_URL = ''; const API_KEY = '^{coreInfo.key}'; const url = `${BASE_URL}/recipe/oauth/clients`; const options = { method: 'POST', headers: { 'api-key': API_KEY, 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ clientName: "", responseTypes: ["code"], grantTypes: ["authorization_code", "refresh_token"], scope: "offline_access ", redirectUris: ["https:///oauth/callback"], }) }; fetch(url, options) .then(response => response.json()) .then(json => console.log(json)) .catch(err => console.error(err)); ``` ```go BASE_URL = "" API_KEY = "^{coreInfo.key}" url = f"{BASE_URL}/recipe/oauth/clients" payload: Dict[str, Any] ={ "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "scope": "offline_access ", "redirectUris": ["https:///oauth/callback"], } headers = { "api-key": API_KEY, "Content-Type": "application/json", } response = requests.post(url, json=payload, headers=headers) print(response.json()) ``` :::caution You have to save the create OAuth2 Client response because this is not persisted internally for security reasons. The information is necessary in the next steps. ::: ### 3. Set up the Authorization Service Backend #### 3.1 Initialize the OAuth2 recipe Update the `supertokens.init` call to include the new recipe. ```tsx supertokens.init({ supertokens: { connectionURI: "...", apiKey: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ OAuth2Provider.init(), ] }); ``` :::caution At the moment, there is no support for creating OAuth2 providers in the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import oauth2provider init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), framework="fastapi", supertokens_config=SupertokensConfig( connection_uri="...", api_key="..." ), recipe_list=[ oauth2provider.init() ], ) ``` #### 3.2 Update the CORS configuration Set up the Backend API to allow requests from all the frontend domains. ```tsx const app = express(); // Add your actual frontend domains here const allowedOrigins = ["", "", ""]; app.use(cors({ // highlight-start origin: allowedOrigins, allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], credentials: true, // highlight-end })); ``` :::caution At the moment, there is no support for creating OAuth2 providers in the Go SDK. ::: ```python from supertokens_python import get_all_cors_headers from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware from supertokens_python.framework.fastapi import get_middleware app = FastAPI() app.add_middleware(get_middleware()) app.add_middleware( CORSMiddleware, # highlight-start allow_origins=[ "", "", "" ], # highlight-end allow_credentials=True, allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], allow_headers=["Content-Type"] + get_all_cors_headers(), ) ``` #### 3.3 Implement a custom session verification function Given that the backend, the **Authorization Server**, also acts as a **Resource Server** you have to account for this in the session verification process. This is necessary because the flow uses two types of tokens: - **SuperTokens Session Access Token**: Used during the login and logout. - **OAuth2 Access Token**: Used to access protected resources and perform actions that need authorization. Hence the logic should distinguish between these two and prevent errors. Here is an example of how to implement this in the context of an Express API: ```tsx interface RequestWithUserId extends Request { userId?: string; } async function verifySession(req: RequestWithUserId, res: Response, next: NextFunction) { let session = undefined; try { session = await Session.getSession(req, res, { sessionRequired: false }); } catch (err) { if ( !Session.Error.isErrorFromSuperTokens(err) || err.type !== Session.Error.TRY_REFRESH_TOKEN ) { return next(err); } } // In this case we are dealing with a SuperTokens Session if (session !== undefined) { const userId = session.getUserId(); req.userId = userId; return next(); } // The OAuth2 Access Token needs to be manually extracted and validated let jwt: string | undefined = undefined; if (req.headers["authorization"]) { jwt = req.headers["authorization"].split("Bearer ")[1]; } if (jwt === undefined) { return next(new Error("No JWT found in the request")); } try { const tokenPayload = await validateToken(jwt, ''); const userId = tokenPayload.sub; req.userId = userId; return next(); } catch (err) { return next(err); } } const JWKS = jose.createRemoteJWKSet( new URL("/jwt/jwks.json"), ); // This is a basic example on how to validate an OAuth2 Token // We have a separate page that talks more in depth about the process async function validateToken(jwt: string, requiredScope: string) { const { payload } = await jose.jwtVerify(jwt, JWKS, { requiredClaims: ["stt", "scp", "sub"], }); if (payload.stt !== 1) throw new Error("Invalid token"); const scopes = payload.scp as string[]; if (!scopes.includes(requiredScope)) throw new Error("Invalid token"); return payload; } // You can then use the function as a middleware for a protected route const app = express(); app.get("/protected", verifySession, async (req, res) => { // Custom logic }); ``` For more information on how to verify the **OAuth2 Access Tokens**, please check the [separate guide](/docs/authentication/unified-login/verify-tokens). :::caution At the moment, there is no support for creating OAuth2 providers in the Go SDK. ::: ```python from supertokens_python.recipe.session.syncio import get_session from supertokens_python.recipe.session.exceptions import SuperTokensSessionError, TryRefreshTokenError from fastapi.requests import Request from typing import List, Optional return True def validate_token(token: str, required_scope: str) -> bool: api_domain = "" api_base_path = "" client_id = "" jwks_url = f"{api_domain}{api_base_path}/jwt/jwks.json" jwks_client = PyJWKClient(jwks_url) try: signing_key = jwks_client.get_signing_key_from_jwt(token) decoded = jwt.decode( token, signing_key.key, algorithms=['RS256'], options={"require": ["stt", "client_id", "scp"]} ) stt: Optional[int] = decoded.get('stt') if stt != 1: return False token_client_id: Optional[str] = decoded.get('client_id', None) if client_id != token_client_id: return False scopes: List[str] = decoded.get('scp', []) if required_scope not in scopes: return False return True except Exception: return False # ``` ### 4. Configure the Authorization Service Frontend #### 4.1 Initialize the recipe Add the import statement for the new recipe and update the list of recipe to also include the new initialization. ```tsx SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ OAuth2Provider.init() ] }); ``` ##### Include the pre-built UI in the rendering tree. ```tsx class App extends React.Component { render() { return ( {/*This renders the login UI on the /auth route*/} {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [OAuth2ProviderPreBuiltUI])} {/*Your app routes*/} ); } } ``` ```tsx class App extends React.Component { render() { if (canHandleRoute([OAuth2ProviderPreBuiltUI])) { // This renders the login UI on the /auth route return getRoutingComponent([OAuth2ProviderPreBuiltUI]) } return ( {/*Your app*/} ); } } ``` Update the `AuthComponent` to include the `OAuth2Provider` recipe. You need to add a new item in the `recipeList` array. ```tsx title="/app/auth/auth.component.ts" @Component({ selector: "app-auth", template: '
', }) export class AuthComponent implements OnDestroy, AfterViewInit { constructor( private renderer: Renderer2, @Inject(DOCUMENT) private document: Document ) { } ngAfterViewInit() { this.loadScript('^{jsdeliver_prebuiltui}'); } ngOnDestroy() { // Remove the script when the component is destroyed const script = this.document.getElementById('supertokens-script'); if (script) { script.remove(); } } private loadScript(src: string) { const script = this.renderer.createElement('script'); script.type = 'text/javascript'; script.src = src; script.id = 'supertokens-script'; script.onload = () => { supertokensUIInit({ appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, recipeList: [ // Don't forget to also include the other recipes that you are already using supertokensUIOAuth2Provider.init() ], }); } this.renderer.appendChild(this.document.body, script); } } ```
Update the `AuthView` component to include the `OAuth2Provider` recipe. You need to add a new item in the `recipeList` array, inside the `supertokensUIInit` call. ```tsx ```
#### 4.2 Disable network interceptors The **Authorization Service Frontend** that you are configuring makes use of two types of access tokens: - **SuperTokens Session Access Token**: Used only during the login flow to keep track of the authentication state. - **OAuth2 Access Token**: Returned after a successful login attempt. It can then access protected resources. By default, the **SuperTokens** frontend SDK intercepts all the network requests sent to your Backend API and adjusts them based on the **SuperTokens Session Tokens**. This allows operations, such as automatic token refreshing or adding authorization headers, without needing to configure anything else. Given that in the scenario you are implementing, the **OAuth2 Access Tokens** serve authorization purposes. The automatic request interception causes conflicts. To prevent this, you need to override the `shouldDoInterceptionBasedOnUrl` function in the `Session.init` call. :::caution The code samples assume that you are using `` as the `apiBasePath` for the backend authentication routes. If that is different please adjust them based on your use case. ::: ```tsx Session.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); // Interception should be done only for routes that need the SuperTokens Session Tokens const isAuthApiRoute = urlObj.pathname.startsWith(""); const isOAuth2ApiRoute = urlObj.pathname.startsWith("/oauth"); if (!isAuthApiRoute || isOAuth2ApiRoute) { return false; } } catch (ignored) { } return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); } } } } }) ``` You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUISession.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("")) { return false; } } catch (ignored) { } return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); } } } } }) ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx Session.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("")) { return false; } } catch (ignored) { } return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); } } } } }) ``` You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUISession.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("")) { return false; } } catch (ignored) { } return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); } } } } }) ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx Session.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("")) { return false; } } catch (ignored) { } return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); } } } } }) ``` The code snippet only allows interception for API endpoints that start with ``. This ensures that calls made from the Frontend SDKs continue to use the **SuperTokens Session Tokens**. As a result, the authentication flow ends up working. For the other routes, you have full control on how you want to attach the **OAuth2 Access Tokens** to the API calls.
The user interface that you are going to build should respect this flow: ## A user accesses your application and tries to login. It's up to you how you want to handle this. They can click a button to login or you can directly start the login flow. ## They get redirected to the **Authorization Service Backend** A **OAuth2/OpenID Connect (OIDC)** library can execute this action. Check the previous guides for information on what you could use. ## The **Authorization Service Backend** redirects them to the **Authorization Service Frontend** login page. The page URL contains a `loginChallenge` parameter that keeps track of the login attempt. Besides that, the URL can also include a `forceFreshAuth` parameter. As the name suggests, this should force the login UI to be visible even though the user has an existing valid session. This guide shows you how to handle this. ## The **Authorization Service Frontend** renders the login UI and the user performs the login action. The login UI should render based on instructions that are specific to each authentication method which you are using. The additional thing that you have to do here is to consider the `forceFreshAuth` parameter. ## The **Authorization Service Frontend** redirects the user back to the **Authorization Service Backend** After the user submits the login form, you need to redirect them to a specific route that sends them to the original application. From here, the authentication flow completes. Let's see how you can actually implement this UI. #### 4.1 Configure the redirection URLs As it has hinted in the previous section, the **Authorization Service Backend** sends the user to different pages from the **Authorization Service Frontend**, based on the action that needs execution. The default values for these routes are: - The login page maps to `` (this is also the place where a user ends up after logout) - The token refresh page maps to `/try-refresh` - The logout page maps to `/logout` If you want to change these routes, you need to add a custom override. :::info This override needs addition to the **Authorization Service Backend**. ::: ```tsx OAuth2Provider.init({ override: { functions: (originalFunctions) => ({ ...originalFunctions, getFrontendRedirectionURL: async (input) => { const websiteDomain = ''; const websiteBasePath = ''; if (input.type === "login") { const queryParams = new URLSearchParams({ loginChallenge: input.loginChallenge, }); if (input.hint !== undefined) { queryParams.set("hint", input.hint); } if (input.forceFreshAuth) { queryParams.set("forceFreshAuth", "true"); } return `?${queryParams.toString()}`; } else if (input.type === "try-refresh") { return `/try-refresh?loginChallenge=${input.loginChallenge}`; } else if (input.type === "post-logout-fallback") { return ``; } else if (input.type === "logout-confirmation") { return `/oauth/logout?logoutChallenge=${input.logoutChallenge}`; } return ``; }, }), }, }) ``` :::caution At the moment, there is no support for creating OAuth2 providers in the Go SDK. ::: :::caution At the moment, there is no support for creating OAuth2 providers in the Python SDK. ::: #### 4.2 Handle the forceFreshAuth parameter Sometimes, even though there is an existing valid session in the **Authorization Service Frontend**, the requesting **Client** might force a new login attempt. The `forceFreshAuth` parameter shows this. When the login page renders, you also need to check for this parameter. You are doing this to know if you need to show the login UI. Here is an example of how you can evaluate this case. ```tsx async function shouldLogin() { const urlParams = new URLSearchParams(window.location.search); const forceFreshAuth = urlParams.get('forceFreshAuth') as string; if(forceFreshAuth === "true") return true; return Session.doesSessionExist(); } ``` :::info Multi Tenancy If you are using multi-tenancy, you also need to keep track of the `tenantId` query parameter and pass it between the **Authorization Service Frontend** pages. ::: #### 4.3 Complete the login attempt After the user submits the login form, you need to redirect them to a specific route to complete the **OAuth 2** flow. The following code sample shows you how to determine which URL to use. ```tsx async function getInitialRedirectionURL() { const urlParams = new URLSearchParams(window.location.search); const loginChallenge = urlParams.get('loginChallenge') as string; const redirectionResponse = await OAuth2Provider.getRedirectURLToContinueOAuthFlow({ loginChallenge }); if (redirectionResponse.status === "OK") { return redirectionResponse.frontendRedirectTo; } } ``` :::caution For mobile apps, you need to reuse the web authentication flow. Check this [guide](/docs/authentication/unified-login/reuse-website-login) for more information. ::: #### 4.4 Add the token refresh page To have support for token refreshing, you need to add a new page to your application. The path should correspond to the one outlined during the first step. When the user ends up on this page, you need to use the `Session` recipe to perform the refresh action. Then they need redirection to a page from your application. Here's a code sample that shows you how to do this. ```tsx async function refreshToken() { await Session.attemptRefreshingSession(); const urlParams = new URLSearchParams(window.location.search); const loginChallenge = urlParams.get('loginChallenge') as string; const redirectionResponse = await OAuth2Provider.getRedirectURLToContinueOAuthFlow({ loginChallenge }); if (redirectionResponse.status === "OK") { window.location.href = redirectionResponse.frontendRedirectTo; } } ``` :::caution For mobile apps, you need to reuse the web authentication flow. Check this [guide](/docs/authentication/unified-login/reuse-website-login) for more information. ::: #### 4.5 Add the logout page You need to add a logout page that users access when they want to end their session. The path should correspond to the one outlined during the first step. The logout action should first ask the user for confirmation. If the confirmation passes, then you can call the recipe function. Based on the final response you can redirect the user to the provided redirection URL. ```tsx async function logout() { const confirmation = confirm("Are you sure that you want to log out?"); if(!confirmation) return; const urlParams = new URLSearchParams(window.location.search); const logoutChallenge = urlParams.get('logoutChallenge') as string; const redirectResponse = await OAuth2Provider.logOut({ logoutChallenge }); window.location.href = redirectResponse.frontendRedirectTo; } ``` :::caution For mobile apps, you need to reuse the web authentication flow. Check this [guide](/docs/authentication/unified-login/reuse-website-login) for more information. ::: ### 5. Update the login flow in your frontend applications Use a generic OAuth2 library to handle the login flow You can use the [react-oidc-context](https://github.com/authts/react-oidc-context) library. Follow the instructions from the library's page. Identify the configuration parameters based on the response received on **step 2**, when creating the **OAuth2 Client**. - `authority` corresponds to the endpoint of the **Authorization Service** `` - `clientID` corresponds to `clientId` - `redirectUri` corresponds to a value from `callbackUrls` - `scope` corresponds to `scope` If you are using a multi-tenant setup, you also need to specify the `tenantId` parameter in the authorization URL. To do this, set the `extraQueryParams` property with a specific value that should look like this: `{ tenant_id: "" }`. You can use the [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) library. Follow the instructions described in the [GitHub repository](https://github.com/manfredsteyer/angular-oauth2-oidc?tab=readme-ov-file#logging-in). Identify the configuration parameters based on the response received on **step 2**, when creating the **OAuth2 Client**. - `issuer` corresponds to the endpoint of the **Authorization Service** `` - `client_id` corresponds to `clientId` - `redirect_uri` corresponds to a value from `callbackUrls` - `scope` corresponds to `scope` If you are using a multi-tenant setup, you also need to specify the `tenantId` parameter in the authorization URL. To do this, set the `extraQueryParams` property with a specific value that should look like this: `{ tenant_id: "" }`. You can use the [oidc-client-ts](https://github.com/authts/oidc-client-ts?tab=readme-ov-file) library. Follow the instructions described in the [GitHub repository](https://github.com/authts/oidc-client-ts/blob/main/docs/protocols/authorization-code-grant-with-pkce.md). Identify the configuration parameters based on the response received on **step 2**, when creating the **OAuth2 Client**. - `issuer` corresponds to the endpoint of the **Authorization Service** `` - `client_id` corresponds to `clientId` - `redirect_uri` corresponds to a value from `callbackUrls` - `scope` corresponds to `scope` If you are using a multi-tenant setup, you also need to specify the `tenantId` parameter in the authorization URL. To do this, set the `extraQueryParams` property with a specific value that should look like this: `{ tenant_id: "" }`. :::info If you want to use the [**OAuth2 Refresh Tokens**](/docs/authentication/unified-login/oauth2-basics#oauth2-refresh-token) make sure to include the `offline_access` scope during the initialization step. ::: ### 6. Test the new authentication flow With everything set up, you can test your login flow. Use the setup created in the previous step to check if the authentication flow completes without any issues. # Authentication - Unified Login - Quickstart Guides - Multiple frontend domains with separate backends Source: https://supertokens.com/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-separate-backends ## Overview You can use the following guide if you have a single [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) that multiple applications use. In turn, each app has separate **`frontend`** and **`backend`** instances that serve from different domains. The authentication flow works in the following way: ## The User accesses the `frontend` app - The application `frontend` calls a login endpoint on the `backend` application. - The `backend` application generates an `authorization` URL to the [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) and redirects the user to it. - The [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) backend redirects the user to the login UI ## The User completes the login attempt - The [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) backend redirects the user to a `callback URL` that includes the **Authorization Code**. ## The user accesses the callback URL - The **Authentication Code** gets sent to the application `backend` - The `backend` exchanges the **Authentication Code** for an [**OAuth2 Access Token**](/docs/authentication/unified-login/oauth2-basics#oauth2-access-token) - The `backend` saves the received token in a server session and sends it back to the `frontend` as a cookie. The `frontend` can use the new cookie to access protected resources from the `backend`. Multiple Frontend Domains with separate Backends ## Before you start These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). If you have skipped that page, please follow the tutorial and return here once you're done. :::info Note that, if the *frontends* and *backends* are in different *sub domains*, you don't need to use *OAuth* and can instead use [session sharing across sub domains](/docs/post-authentication/session-management/share-session-across-sub-domains). ::: ## Steps ### 1. Enable the Unified Login feature Go to the [**SuperTokens.com SaaS Dashboard**](https://supertokens.com) and follow these instructions: 1. Click on the **Enabled Paid Features** button 2. Click on **Managed Service** 3. Check the **Unified Login / M2M** option 4. Click *Save* ### 2. Create the OAuth2 Clients For each of your applications you need to create a separate [**OAuth2 client**](/docs/authentication/unified-login/oauth2-basics#client). You can do this by directly calling the **SuperTokens Core** API. ```bash curl --location --request POST '/recipe/oauth/clients' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data ' { "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "scope": "offline_access ", "redirectUris": ["https:///oauth/callback"], } ' ``` ```tsx const BASE_URL = ''; const API_KEY = '^{coreInfo.key}'; const url = `${BASE_URL}/recipe/oauth/clients`; const options = { method: 'POST', headers: { 'api-key': API_KEY, 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ clientName: "", responseTypes: ["code"], grantTypes: ["authorization_code", "refresh_token"], scope: "offline_access ", redirectUris: ["https:///oauth/callback"], }) }; fetch(url, options) .then(response => response.json()) .then(json => console.log(json)) .catch(err => console.error(err)); ``` ```go BASE_URL = "" API_KEY = "^{coreInfo.key}" url = f"{BASE_URL}/recipe/oauth/clients" payload: Dict[str, Any] ={ "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "scope": "offline_access ", "redirectUris": ["https:///oauth/callback"], } headers = { "api-key": API_KEY, "Content-Type": "application/json", } response = requests.post(url, json=payload, headers=headers) print(response.json()) ``` :::caution You have to save the create OAuth2 Client response because this is not persisted internally for security reasons. The information is necessary for the next steps. ::: ### 3. Set Up your Authorization Service backend In your [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) you need to initialize the **OAuth2Provider** recipe. The recipe exposes the endpoints needed for enabling the [**OAuth 2.0**](/docs/authentication/unified-login/oauth2-basics) flow. Update the `supertokens.init` call to include the `OAuth2Provider` recipe. Add the import statement for the recipe and update the list of recipes with the new initialization step. ```tsx supertokens.init({ supertokens: { connectionURI: "...", apiKey: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ OAuth2Provider.init(), ] }); ``` :::caution At the moment there is no support for creating OAuth2 providers in the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import oauth2provider init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), framework="fastapi", supertokens_config=SupertokensConfig( connection_uri="...", api_key="..." ), recipe_list=[ oauth2provider.init() ], ) ``` ### 4. Configure the Authorization Service frontend #### 4.1 Initialize the recipe Add the import statement for the new recipe and update the list of recipe to also include the new initialization. ```tsx SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ OAuth2Provider.init() ] }); ``` #### 4.2 Include the pre-built UI in the rendering tree. ```tsx class App extends React.Component { render() { return ( {/*This renders the login UI on the /auth route*/} {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [OAuth2ProviderPreBuiltUI])} {/*Your app routes*/} ); } } ``` ```tsx class App extends React.Component { render() { if (canHandleRoute([OAuth2ProviderPreBuiltUI])) { // This renders the login UI on the /auth route return getRoutingComponent([OAuth2ProviderPreBuiltUI]) } return ( {/*Your app*/} ); } } ``` Update the `AuthComponent` to include the `OAuth2Provider` recipe. You need to add a new item in the `recipeList` array. ```tsx title="/app/auth/auth.component.ts" @Component({ selector: "app-auth", template: '
', }) export class AuthComponent implements OnDestroy, AfterViewInit { constructor( private renderer: Renderer2, @Inject(DOCUMENT) private document: Document ) { } ngAfterViewInit() { this.loadScript('^{prebuiltUIVersion}'); } ngOnDestroy() { // Remove the script when the component is destroyed const script = this.document.getElementById('supertokens-script'); if (script) { script.remove(); } } private loadScript(src: string) { const script = this.renderer.createElement('script'); script.type = 'text/javascript'; script.src = src; script.id = 'supertokens-script'; script.onload = () => { supertokensUIInit({ appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, recipeList: [ // Don't forget to also include the other recipes that you are already using supertokensUIOAuth2Provider.init() ], }); } this.renderer.appendChild(this.document.body, script); } } ```
Update the `AuthView` component to include the `OAuth2Provider` recipe. You need to add a new item in the `recipeList` array, inside the `supertokensUIInit` call. ```tsx ```
The user interface that you are going to build should respect this flow: ## A user accesses your application and tries to login. It's up to you how you want to handle this. They can click a button to login or you can directly start the login flow. ## They get redirected to the **Authorization Service Backend** A **OAuth2/OpenID Connect (OIDC)** library can execute this action. Check the previous guides for information on what you could use. ## The **Authorization Service Backend** redirects them to the **Authorization Service Frontend** login page. The page URL contains a `loginChallenge` parameter that keeps track of the login attempt. Besides that, the URL can also include a `forceFreshAuth` parameter. As the name suggests, this should force the login UI to be visible even though the user has an existing valid session. This guide shows you how to handle this. ## The **Authorization Service Frontend** renders the login UI and the user performs the login action. The login UI should render based on instructions that are specific to each authentication method which you are using. The additional thing that you have to do here is to consider the `forceFreshAuth` parameter. ## The **Authorization Service Frontend** redirects the user back to the **Authorization Service Backend** After the user submits the login form, you need to redirect them to a specific route that sends them to the original application. From here, the authentication flow completes. Let's see how you can actually implement this UI. #### 4.1 Configure the redirection URLs As it has hinted in the previous section, the **Authorization Service Backend** sends the user to different pages from the **Authorization Service Frontend**, based on the action that needs execution. The default values for these routes are: - The login page maps to `` (this is also the place where a user ends up after logout) - The token refresh page maps to `/try-refresh` - The logout page maps to `/logout` If you want to change these routes, you need to add a custom override. :::info This override needs addition to the **Authorization Service Backend**. ::: ```tsx OAuth2Provider.init({ override: { functions: (originalFunctions) => ({ ...originalFunctions, getFrontendRedirectionURL: async (input) => { const websiteDomain = ''; const websiteBasePath = ''; if (input.type === "login") { const queryParams = new URLSearchParams({ loginChallenge: input.loginChallenge, }); if (input.hint !== undefined) { queryParams.set("hint", input.hint); } if (input.forceFreshAuth) { queryParams.set("forceFreshAuth", "true"); } return `?${queryParams.toString()}`; } else if (input.type === "try-refresh") { return `/try-refresh?loginChallenge=${input.loginChallenge}`; } else if (input.type === "post-logout-fallback") { return ``; } else if (input.type === "logout-confirmation") { return `/oauth/logout?logoutChallenge=${input.logoutChallenge}`; } return ``; }, }), }, }) ``` :::caution At the moment, there is no support for creating OAuth2 providers in the Go SDK. ::: :::caution At the moment, there is no support for creating OAuth2 providers in the Python SDK. ::: #### 4.2 Handle the forceFreshAuth parameter Sometimes, even though there is an existing valid session in the **Authorization Service Frontend**, the requesting **Client** might force a new login attempt. The `forceFreshAuth` parameter shows this. When the login page renders, you also need to check for this parameter. You are doing this to know if you need to show the login UI. Here is an example of how you can evaluate this case. ```tsx async function shouldLogin() { const urlParams = new URLSearchParams(window.location.search); const forceFreshAuth = urlParams.get('forceFreshAuth') as string; if(forceFreshAuth === "true") return true; return Session.doesSessionExist(); } ``` :::info Multi Tenancy If you are using multi-tenancy, you also need to keep track of the `tenantId` query parameter and pass it between the **Authorization Service Frontend** pages. ::: #### 4.3 Complete the login attempt After the user submits the login form, you need to redirect them to a specific route to complete the **OAuth 2** flow. The following code sample shows you how to determine which URL to use. ```tsx async function getInitialRedirectionURL() { const urlParams = new URLSearchParams(window.location.search); const loginChallenge = urlParams.get('loginChallenge') as string; const redirectionResponse = await OAuth2Provider.getRedirectURLToContinueOAuthFlow({ loginChallenge }); if (redirectionResponse.status === "OK") { return redirectionResponse.frontendRedirectTo; } } ``` :::caution For mobile apps, you need to reuse the web authentication flow. Check this [guide](/docs/authentication/unified-login/reuse-website-login) for more information. ::: #### 4.4 Add the token refresh page To have support for token refreshing, you need to add a new page to your application. The path should correspond to the one outlined during the first step. When the user ends up on this page, you need to use the `Session` recipe to perform the refresh action. Then they need redirection to a page from your application. Here's a code sample that shows you how to do this. ```tsx async function refreshToken() { await Session.attemptRefreshingSession(); const urlParams = new URLSearchParams(window.location.search); const loginChallenge = urlParams.get('loginChallenge') as string; const redirectionResponse = await OAuth2Provider.getRedirectURLToContinueOAuthFlow({ loginChallenge }); if (redirectionResponse.status === "OK") { window.location.href = redirectionResponse.frontendRedirectTo; } } ``` :::caution For mobile apps, you need to reuse the web authentication flow. Check this [guide](/docs/authentication/unified-login/reuse-website-login) for more information. ::: #### 4.5 Add the logout page You need to add a logout page that users access when they want to end their session. The path should correspond to the one outlined during the first step. The logout action should first ask the user for confirmation. If the confirmation passes, then you can call the recipe function. Based on the final response you can redirect the user to the provided redirection URL. ```tsx async function logout() { const confirmation = confirm("Are you sure that you want to log out?"); if(!confirmation) return; const urlParams = new URLSearchParams(window.location.search); const logoutChallenge = urlParams.get('logoutChallenge') as string; const redirectResponse = await OAuth2Provider.logOut({ logoutChallenge }); window.location.href = redirectResponse.frontendRedirectTo; } ``` :::caution For mobile apps, you need to reuse the web authentication flow. Check this [guide](/docs/authentication/unified-login/reuse-website-login) for more information. ::: ### 5. Set up session handling in each application In each of your individual `applications` you need to set up logic for handling the **OAuth 2.0** authentication flow. You can use a generic **OIDC** or **OAuth2** library to do this. You can use the [passport-oauth2](https://www.passportjs.org/packages/passport-oauth2/) library. Follow the instructions on the library's page and set up your application `backend`. You can determine the configuration parameters based on the response received in **step 2**, when creating the **OAuth2 Client**. - `authorizationURL` corresponds to `authorizeUrl` - `tokenURL` corresponds to `tokenFetchUrl` - `clientID` corresponds to `clientId` - `clientSecret` corresponds to `clientSecret` - `callbackURL` corresponds to a value from `callbackUrls` - `scope` corresponds to `scope` Make sure that you expose an endpoint that calls `passport.authenticate('oauth2')`. This way the user ends up accessing the actual login page served by the **Authorization Service**. You can use the [OAuth2](https://pkg.go.dev/golang.org/x/oauth2) library. Follow these [instructions](https://golang.org/pkg/golang.org/x/oauth2/#example-Config-RequestToken) and implement it in your `backend`. You can determine the configuration parameters based on the response received in **step 2**. - `ClientID` corresponds to `clientId` - `ClientSecret` corresponds to `clientSecret` - `Scopes` corresponds to `scope` - `Endpoint.AuthURL` corresponds to `authorizeUrl` - `Endpoint.TokenURL` corresponds to `tokenFetchUrl` Make sure that you expose an endpoint that redirects to the authentication URL obtained from calling `AuthCodeURL`. This way the user ends up accessing the actual login page served by the **Authorization Service**. You can use the [AuthLib](https://docs.authlib.org/) library. Follow these [instructions](https://docs.authlib.org/en/latest/client/oauth2.html) and implement it in your `backend`. You can determine the configuration parameters based on the response received in **step 2**. - `client_id` corresponds to `clientId` - `client_secret` corresponds to `clientSecret` - `scope` corresponds to `scope` - `authorization_endpoint` corresponds to `authorizeUrl` - `token_endpoint` corresponds to `tokenFetchUrl` Make sure that you expose an endpoint that redirects to the authentication URL obtained from calling `create_authorization_url`. This way the user ends up accessing the actual login page served by the **Authorization Service**. You can use the [League OAuth2 Client](https://github.com/thephpleague/oauth2-client) library. Follow these [instructions](https://oauth2-client.thephpleague.com/usage/) and implement it in your `backend`. You can determine the configuration parameters based on the response received in **step 2**. - `clientId` corresponds to `clientId` - `clientSecret` corresponds to `clientSecret` - `redirectUri` corresponds to a value from `callbackUrls` - `urlAuthorize` corresponds to `authorizeUrl` - `urlAccessToken` corresponds to `tokenFetchUrl` Make sure that you expose an endpoint that redirects to the authentication URL obtained from calling `getAuthorizationUrl`. This way the user ends up accessing the actual login page served by the **Authorization Service**. You can use the [Spring Security](https://github.com/spring-projects/spring-security) library. Follow these [instructions](https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html#oauth2-client-log-users-in) and implement it in your `backend`. You can determine the configuration parameters based on the response received in **step 2**. - `client-id` corresponds to `clientId` - `client-secret` corresponds to `clientSecret` - `scope` corresponds to `scope` - `issuer-uri` corresponds to `` You can use the [IdentityModel](https://github.com/IdentityModel/IdentityModel) library. Follow these [instructions](https://identitymodel.readthedocs.io/en/latest/client/token.html#requesting-a-token-using-the-authorization-code-grant-type) and implement it in your `backend`. You can determine the configuration parameters based on the response received in **step 2**. - `Address` corresponds to `` - `ClientId` corresponds to `clientId` - `ClientSecret` corresponds to `clientSecret` - `RedirectUri` corresponds to a value from `callbackUrls` Make sure that you expose an endpoint that redirects to the authentication URL obtained by using [this example](https://identitymodel.readthedocs.io/en/latest/misc/request_url.html#authorization-endpoint). This way the user ends up accessing the actual login page served by the **Authorization Service**. :::info If you want to use the [**OAuth2 Refresh Tokens**](/docs/authentication/unified-login/oauth2-basics#oauth2-refresh-token) make sure to include the `offline_access` scope during the initialization step. ::: ### 6. Update the login flow in your frontend applications In your `frontend` applications you need to add a login action that directs the user to the authentication page. The user should first redirect to the `backend` authentication endpoint defined during the previous step. There the `backend` generates a safe `authorization` URL using the **OAuth2** library and then redirects the user there. After the user has logged in from the [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) they redirect to the `backend` callback URL. Then the `backend` creates the authentication session and sends it to the user agent as a cookie. ### 7. Test the new authentication flow With everything set up, you can test your login flow. Use the setup created in the previous step to check if the authentication flow completes without any issues. # Authentication - Unified Login - Quickstart Guides - Reuse website login for desktop and mobile apps Source: https://supertokens.com/docs/authentication/unified-login/quickstart-guides/reuse-website-login ## Overview This pattern is useful if you want to have the same web authentication experience for your desktop and mobile apps. The implementation allows you to save development time but keep in mind that it does not involve a native authentication interface. Users get directed to a separate browser page where they complete the authentication flow and then return to your application. The authentication flow works in the following way: ## User accesses the native application - The user gets redirected to [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) authentication URL. - The [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) redirects the user to the login UI ## User completes the login attempt - The [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) backend redirects the user to the `callback URL`. - This URL should be a deep link that your actual application can open. ## The application uses the callback URL information to request a **OAuth2 Access Token** from the [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server). - The application saves the returned token. Reuse website login for desktop and mobile apps ## Before you start These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). If you have skipped that page, please follow the tutorial and return here once you're done. ## Steps ### 1. Enable the Unified Login feature Go to the [**SuperTokens.com SaaS Dashboard**](https://supertokens.com) and follow these instructions: 1. Click on the **Enabled Paid Features** button 2. Click on **Managed Service** 3. Check the **Unified Login / M2M** option 4. Click *Save* ### 2. Create the OAuth2 Clients For each of your applications you need to create a separate [**OAuth2 client**](/docs/authentication/unified-login/oauth2-basics#client). You can do this by directly calling the **SuperTokens Core** API. ```bash curl --location --request POST '/recipe/oauth/clients' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data ' { "clientName": "", "responseTypes": ["code", "id_token"], "grantTypes": ["authorization_code", "refresh_token"], "tokenEndpointAuthMethod": "none", "scope": "offline_access ", "redirectUris": ["https:///oauth/callback"], } ' ``` ```tsx const BASE_URL = ''; const API_KEY = '^{coreInfo.key}'; const url = `${BASE_URL}/recipe/oauth/clients`; const options = { method: 'POST', headers: { 'api-key': API_KEY, 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ clientName: "", responseTypes: ["code", "id_token"], grantTypes: ["authorization_code", "refresh_token"], tokenEndpointAuthMethod: "none", scope: "offline_access ", redirectUris: ["https:///oauth/callback"], }) }; fetch(url, options) .then(response => response.json()) .then(json => console.log(json)) .catch(err => console.error(err)); ``` ```go BASE_URL = "" API_KEY = "^{coreInfo.key}" url = f"{BASE_URL}/recipe/oauth/clients" payload: Dict[str, Any] ={ "clientName": "", "responseTypes": ["code", "id_token"], "grantTypes": ["authorization_code", "refresh_token"], "tokenEndpointAuthMethod": "none", "scope": "offline_access ", "redirectUris": ["https:///oauth/callback"], } headers = { "api-key": API_KEY, "Content-Type": "application/json", } response = requests.post(url, json=payload, headers=headers) print(response.json()) ``` :::caution You have to save the create OAuth2 Client response because this is not persisted internally for security reasons. The information is necessary for the next steps. ::: Based on the client creation process, you can infer two additional values that you need later on: - `authorizeUrl` corresponds to `/oauth/auth` - `tokenFetchUrl` corresponds to `/oauth/token` ### 3. Configure the Authorization Service Check one of the previous guides that show you how to set up the **Authorization Service** and then return to this page. Choose the tutorial based whether you use multiple backend services or not: - [Single Backend Setup](/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-a-single-backend#3-set-up-the-authorization-service-backend) - [Multiple Backends Setup](/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-a-single-backend#3-set-up-the-authorization-service-backend) ### 4. Update the login flow in your applications In each of your individual `applications`, you need to set up logic for handling the **OAuth 2.0** authentication flow. You can use a generic **OIDC** or **OAuth** library to do this. You can use the [react-native-app-auth](https://commerce.nearform.com/open-source/react-native-app-auth/) library. Follow [the instructions](https://commerce.nearform.com/open-source/react-native-app-auth/docs/usage/config) to set up your application. You can identify the configuration parameters based on the response received in **step 2**, when creating the **OAuth2 Client**. - `issuer` corresponds to the endpoint of the **Authorization Service** `` - `clientID` corresponds to `clientId` - `redirectUrl` corresponds a value from `callbackUrls` - `scopes` corresponds to `scopes` You also need to set the `additionalParameters` property with the following values: - `max_age: 0` This forces a new authentication flow once the user ends up on the **Authorization Service** frontend. - `tenant_id: ` Optional, in case you are using a multi tenant setup. Set this to the actual tenant ID. You can use the [AppAuth-Android](https://github.com/openid/AppAuth-Android) library. Follow [the instructions](https://github.com/openid/AppAuth-Android?tab=readme-ov-file#authorization-service-configuration) to set up your application. You can identify the configuration parameters based on the response received in **step 2**, when creating the **OAuth2 Client**. For the `AuthorizationServiceConfiguration`, the parameters you need to provide are: `authorizeUrl` and `tokenFetchUrl`. When calling the `AuthorizationRequest.Builder` function you can use `clientId` and a value from `callbackUrls` to replace the example values. You need to set additional query parameters by calling the `setAdditionalParameters` function on the `AuthorizationRequest.Builder` object: - `max_age: 0` This forces a new authentication flow once the user ends up on the **Authorization Service** frontend. - `tenant_id: ` Optional, in case you are using a multi tenant setup. Set this to the actual tenant ID. You can use the [AppAuth-iOS](https://github.com/openid/AppAuth-Android) library. Follow [the instructions](https://github.com/openid/AppAuth-iOS?tab=readme-ov-file#auth-flow) to set up your application. You can identify the configuration parameters based on the response received in **step 2**, when creating the **OAuth2 Client**. - `clientID` corresponds to `clientId` - `redirectUrl` corresponds a value from `callbackUrls` - `scopes` corresponds to `scopes` - `authorizationEndpoint` corresponds to `authorizeUrl` - `tokenEndpoint` corresponds to `tokenFetchUrl` You also need to set extra query parameters, when instantiating the `OIDAuthorizationRequest` object, with the following values: - `max_age: 0` This forces a new authentication flow once the user ends up on the **Authorization Service** frontend. - `tenant_id: ` Optional, in case you are using a multi tenant setup. Set this to the actual tenant ID. You can use the [AppAuth](https://github.com/MaikuB/flutter_appauth) library. Follow [the instructions](https://github.com/MaikuB/flutter_appauth/tree/master/flutter_appauth) to set up your application. You can identify the configuration parameters based on the response received in **step 2**, when creating the **OAuth2 Client**. - `` corresponds to `clientId` - `` corresponds to the endpoint of the **Authorization Service** `` - `` corresponds a value from `callbackUrls` - `scopes` corresponds to `scopes` You also need to set the `additionalParameters` property with the following values: - `max_age: 0` This forces a new authentication flow once the user ends up on the **Authorization Service** frontend. - `tenant_id: ` Optional, in case you are using a multi tenant setup. Set this to the actual tenant ID. :::info If you want to use the [**OAuth2 Refresh Tokens**](/docs/authentication/unified-login/oauth2-basics#oauth2-refresh-token) make sure to include the `offline_access` scope during the initialization step. ::: ### 5. Test the new authentication flow With everything set up, you can test your login flow. Use the setup created in the previous step to check if the authentication flow completes without any issues. # Authentication - Unified Login - Work with scopes Source: https://supertokens.com/docs/authentication/unified-login/work-with-scopes ## Overview The creation process of an **OAuth2 Client** determines the allowed scopes. By default, the **OAuth2** implementation adds the following built-in scopes: | Scope | Claims Added | Notes | |-------|-------------|--------| | `email` | `email`, `emails`, `email_verified` | Added to ID Token and User Info | | `phoneNumber` | `phoneNumber`, `phoneNumbers`, `phoneNumber_verified` | Added to ID Token and User Info | | `roles` | The roles return by `getRolesForUser` | Added to ID Token and Access Token | | `permissions` | The list of permissions obtained by concatenating the result of `getPermissionsForRole` for all roles returned by `getRolesForUser` | Added to ID Token and Access Token | --- ## Request specific scopes The client can request specific scopes by adding `scope` query parameter to the **Authorization URL**. The requested scopes have to be a subset of what the client allows, otherwise the authentication request fails. By default, the client receives all scopes. --- ## Override granted scopes If you want to manually modify the list of scopes that the client receives during the authentication flow, you can do this by using overrides. ```tsx OAuth2Provider.init({ override: { functions: (originalFunctions) => ({ ...originalFunctions, getRequestedScopes: async (input) => { const originallyRequestedScopes = await originalFunctions.getRequestedScopes(input); const filteredScopes = originallyRequestedScopes.filter((scope) => scope !== "profile"); return [...filteredScopes, "custom-scope"]; }, }), }, }); ``` :::caution The Go SDK does not support creating OAuth2 providers. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import oauth2provider from supertokens_python.recipe.oauth2provider.interfaces import RecipeInterface from supertokens_python.types import RecipeUserId from typing import Dict, List, Any, Optional def override_oauth2provider_functions(original_implementation: RecipeInterface): original_get_requested_scopes = original_implementation.get_requested_scopes async def get_requested_scopes( recipe_user_id: Optional[RecipeUserId], session_handle: Optional[str], scope_param: List[str], client_id: str, user_context: Dict[str, Any], ): originally_requested_scopes = await original_get_requested_scopes( recipe_user_id, session_handle, scope_param, client_id, user_context ) filtered_scopes = [scope for scope in originally_requested_scopes if scope != "profile"] return [*filtered_scopes, "custom-scope"] original_implementation.get_requested_scopes = get_requested_scopes return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), framework="fastapi", supertokens_config=SupertokensConfig( connection_uri="...", api_key="..." ), recipe_list=[ oauth2provider.init( override=oauth2provider.InputOverrideConfig(functions=override_oauth2provider_functions) ) ], ) ``` # Authentication - Unified Login - Verify tokens Source: https://supertokens.com/docs/authentication/unified-login/verify-tokens ## Overview You can verify an **OAuth2 Access Token** in two ways: - By using a standard **JWT**, JSON Web Token, verification library - By calling the special introspection endpoint that gets exposed through the core service One thing to note is that, besides the standard **OAuth2** token claims, the **Unified Login** implementation includes an additional one called `stt`. This stands for `SuperTokens Token Type`. It ensures that the validation occurs for the correct token type: - `0` represents a **SuperTokens Session Access Token** - `1` represents an **OAuth2 Access Token** - `2` represents an **OAuth2 ID Token**. :::caution no-title The following guide covers only **OAuth2 Tokens** verification. For information on how to verify **SuperTokens Session Tokens** please refer to the [following section](/docs/additional-verification/session-verification/protect-api-routes). ::: --- ## Using a JWT verification library This is the standard validation method and you should use it for most of the operations that need protection. It indicates that the token is valid from a cryptographic standpoint and that it has not expired. The code samples show you a basic validation scenario where the token undergoes a check for the required scope for an action to occur. :::info If your scenario involves a common backend with multiple frontend clients you can drop the `client_id` check. ::: For NodeJS you can use [`jose`](https://github.com/panva/jose) to verify the token. ```tsx const JWKS = jose.createRemoteJWKSet(new URL('jwt/jwks.json')) // Follow this example if you are using the Authorization Code Flow async function validateToken(jwt: string) { const requiredScope = ""; const clientId = ''; try { const { payload } = await jose.jwtVerify(jwt, JWKS, { requiredClaims: ['stt', 'scp', 'client_id'], }); if(payload.stt !== 1) return false; if(payload.client_id !== clientId) return false; const scopes = payload.scp as string[]; return scopes.includes(requiredScope); } catch (err) { return false; } } ``` You can use the [`jwx`](https://github.com/lestrrat-go/jwx) library to verify the token. ```go return true } } return false; } ``` You can use the [PyJWT](https://github.com/jpadilla/pyjwt) library to verify the token. ```python from typing import Optional, List use Firebase\JWT\JWT; use Firebase\JWT\Key; function validateToken($jwt) { $apiDomain = ""; $apiBasePath = ""; $jwksUrl = $apiDomain . $apiBasePath . '/jwt/jwks.json'; $requiredScope = ""; $clientId = ""; $jwks = json_decode(file_get_contents($jwksUrl), true); try { $decoded = JWT::decode($jwt, JWK::parseKeySet($jwks), 'RS256')); if ($decoded->sst !== 1) { return false; } if ($decoded->client_id !== $clientId) { return false; } return in_array($requiredScope, $decoded->scp); } catch (Exception $e) { return false; } } ``` You can use the [Auth0 JWT](https://github.com/auth0/java-jwt) library to verify the token. Here is an example of how you can do that: ```java public class JWTVerifier { private static final String JWKS_URL = "jwt/jwks.json"; private static final String CLIENT_ID = ""; private static Map fetchJWKS() throws Exception { URL url = new URL(JWKS_URL); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); InputStream responseStream = connection.getInputStream(); Scanner scanner = new Scanner(responseStream, StandardCharsets.UTF_8.name()); String responseBody = scanner.useDelimiter("\\A").next(); scanner.close(); return JWT.decode(responseBody).getHeader(); } public static boolean validateToken(String token) { try { Map jwks = fetchJWKS(); Algorithm algorithm = Algorithm.RSA256(jwks.get("x5c"), null); JWTVerifier verifier = JWT.require(algorithm) .build(); DecodedJWT jwt = verifier.verify(token); if(jwt.getClaim("sst").asInt() != 1) { return false; } if(jwt.getClaim("client_id").asString() != CLIENT_ID) { return false; } List scopes = jwt.getClaim("scp").asList(); return scopes.contains(requiredScope); } catch (Exception e) { return false; } } } ``` You can use the [IdentityModel](https://github.com/IdentityModel/IdentityModel) library to verify the token. ```csharp using System; using System.Linq; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Net.Http; using System.Threading.Tasks; using Newtonsoft.Json.Linq; class AuthorizationCodeTokenValidator { static async Task ValidateToken(string jwtStr) { string apiDomain = ""; string apiBasePath = ""; string clientId = ""; string requiredScope = ""; HttpClient client = new HttpClient(); var response = await client.GetStringAsync($"//jwt/jwks.json"); var jwks = new JsonWebKeySet(response); var tokenHandler = new JwtSecurityTokenHandler(); var validationParameters = new TokenValidationParameters { IssuerSigningKeys = jwks.Keys }; try { SecurityToken validatedToken; var principal = tokenHandler.ValidateToken(jwtStr, validationParameters, out validatedToken); var claims = principal.Claims.ToDictionary(c => c.Type, c => c.Value); if (!claims.ContainsKey("stt") || claims["stt"] != "1") { return false; } if (!claims.ContainsKey("client_id") || claims["client_id"] != clientId) { return false; } var scopes = claims["scp"].Split(" "); if (!scopes.Contains(requiredScope)) { return false; } return true; } catch (Exception) { return false; } } } ``` ### Email verification If you are using email and password based authentication, and you want to validate if the user has verified their email, you must check if the `email_verified` claim is true. --- ## Using the token introspection API When a user logs out, their token gets removed from the **SuperTokens Core** database. That change does not reflect in token validation process that uses a JWT verification library. The token remains valid until its expiration time. To ensure that the token remains valid, you can directly call the **SuperTokens Core** service. It is advisable to perform this process for high security operations to avoid the risk of a malicious agent using a token. Here is an example of how you can use this validation method: ```tsx async function validateToken(token: string) { const { status } = await OAuth2Provider.validateOAuth2AccessToken( token, { clientId: "", scopes: [""], }, true ); return status === "OK"; } ``` :::caution The Go SDK does not support creating OAuth2 providers. ::: ```python from supertokens_python.recipe.oauth2provider.interfaces import OAuth2TokenValidationRequirements # Authentication - Unified Login - Add custom claims in tokens Source: https://supertokens.com/docs/authentication/unified-login/add-custom-claims-in-tokens ## Overview If you want to add custom properties in the token payloads you can do this by using overrides. --- ## Add claims in the OAuth2 Access Token Override the `buildAccessTokenPayload` function to include the custom claims. ```tsx OAuth2Provider.init({ override: { functions: (originalImplementation) => ({ ...originalImplementation, buildAccessTokenPayload: async (input) => { const addedInfo: Record = {}; if (input.scopes.includes("profile")) { addedInfo.profile = "custom-value"; } return { ...(await originalImplementation.buildAccessTokenPayload(input)), ...addedInfo, }; }, }), }, }); ``` :::caution At the moment there is no support for creating OAuth2 providers in the Go SDK. ::: Override the `build_access_token_payload` function to include the custom claims. ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import oauth2provider from supertokens_python.recipe.oauth2provider.oauth2_client import OAuth2Client from supertokens_python.recipe.oauth2provider.interfaces import RecipeInterface from supertokens_python.types import User from typing import Dict, List, Any, Optional def override_oauth2provider_functions(original_implementation: RecipeInterface): original_build_access_token_payload = original_implementation.build_access_token_payload async def build_access_token_payload( user: Optional[User], client: OAuth2Client, session_handle: Optional[str], scopes: List[str], user_context: Dict[str, Any], ) -> Dict[str, Any]: added_info = {} if "profile" in scopes: added_info['profile'] = "custom-value" original_payload = await original_build_access_token_payload( user, client, session_handle, scopes, user_context ) return {**original_payload, **added_info} original_implementation.build_access_token_payload = build_access_token_payload return original_implementation init( framework="...", # type: ignore app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", api_key="..." ), recipe_list=[ oauth2provider.init( override=oauth2provider.InputOverrideConfig(functions=override_oauth2provider_functions) ) ], ) ``` --- ## Add claims in the ID Token Override the `buildIdTokenPayload` function to include the custom claims. ```tsx OAuth2Provider.init({ override: { functions: (originalImplementation) => ({ ...originalImplementation, buildIdTokenPayload: async (input) => { const addedInfo: Record = {}; if (input.scopes.includes("profile")) { addedInfo.profile = "custom-value"; } return { ...(await originalImplementation.buildIdTokenPayload(input)), ...addedInfo, }; }, }), }, }); ``` :::caution At the moment there is no support for creating OAuth2 providers in the Go SDK. ::: Override the `build_id_token_payload` function to include the custom claims. ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import oauth2provider from supertokens_python.recipe.oauth2provider.oauth2_client import OAuth2Client from supertokens_python.recipe.oauth2provider.interfaces import RecipeInterface from supertokens_python.types import User from typing import Dict, List, Any, Optional def override_oauth2provider_functions(original_implementation: RecipeInterface): original_build_id_token_payload = original_implementation.build_id_token_payload async def build_id_token_payload( user: Optional[User], client: OAuth2Client, session_handle: Optional[str], scopes: List[str], user_context: Dict[str, Any], ) -> Dict[str, Any]: added_info = {} if "profile" in scopes: added_info['profile'] = "custom-value" original_payload = await original_build_id_token_payload( user, client, session_handle, scopes, user_context ) return {**original_payload, **added_info} original_implementation.build_id_token_payload = build_id_token_payload return original_implementation init( framework="...", # type: ignore app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", api_key="..." ), recipe_list=[ oauth2provider.init( override=oauth2provider.InputOverrideConfig(functions=override_oauth2provider_functions) ) ], ) ``` # Authentication - Machine to Machine - Client credentials authentication Source: https://supertokens.com/docs/authentication/m2m/client-credentials ## Overview In the **Client Credentials Flow** the authentication sequence works in the following way: ## `Service A` uses credentials to get an **OAuth2 Access Token** ## [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) returns the **OAuth2 Access Token** ## `Service A` uses the **OAuth2 Access Token** to communicate with `Service B` ## `Service B` validates the **OAuth2 Access Token** ## If the token is valid `Service B` returns the requested resource Machine to Machine Authentication Before going into the actual instructions, start by imagining a real life example that you can reference along the way. This makes it easier to understand what is happening. We are going to configure authentication for the following setup: - A **Calendar Service** that exposes these actions: `event.view`, `event.create`, `event.update` and `event.delete` - A **File Service** that exposes these actions: `file.view`, `file.create`, `file.update` and `file.delete` - A **Task Service** that interacts with the **Calendar Service** and the **File Service** in the process of scheduling a task The aim is to allow the **Task Service** to perform an authenticated action on the **Calendar Service**. Proceed to the actual steps. ## Before you start These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). If you have skipped that page, please follow the tutorial and return here once you're done. ## Steps ### 1. Enable the OAuth2 features from the Dashboard You first have to enable the **OAuth2** features from the **SuperTokens.com Dashboard**. 1. Open the **SuperTokens.com Dashboard** 2. Click on the **Enabled Paid Features** button 3. Click on **Managed Service** 4. Check the **Unified Login / M2M** option 5. Click *Save* You should be able to use the OAuth2 recipes in your applications. ### 2. Create the OAuth2 Clients For each of your **`microservices`** you need to create a separate [**OAuth2 client**](/docs/authentication/unified-login/oauth2-basics#client). This can occur by directly calling the **SuperTokens Core** API. ```bash curl --location --request POST '/recipe/oauth/clients' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data ' { "clientName": "", "grantTypes": ["client_credentials"], "scope": " ", "audience": [""], } ' ``` ```tsx const BASE_URL = ''; const API_KEY = '^{coreInfo.key}'; const url = `${BASE_URL}/recipe/oauth/clients`; const options = { method: 'POST', headers: { 'api-key': API_KEY, 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ clientName: "", grantTypes: ["client_credentials"], scope: " ", audience: [""], }) }; fetch(url, options) .then(response => response.json()) .then(json => console.log(json)) .catch(err => console.error(err)); ``` ```go BASE_URL = "" API_KEY = "^{coreInfo.key}" url = f"{BASE_URL}/recipe/oauth/clients" payload: Dict[str, Any] ={ "clientName": "", "grantTypes": ["client_credentials"], "scope": "custom_scope_1> ", "audience": [""], } headers = { "api-key": API_KEY, "Content-Type": "application/json", } response = requests.post(url, json=payload, headers=headers) print(response.json()) ``` :::info Custom Example To create a client for the **Task Service**, use the following attributes: ```json { "clientName": "Task Service", "grantTypes": ["client_credentials"], "scope": "event.view event.create event.edit event.delete file.view file.create file.edit file.delete", "audience": ["event", "file"] } ``` This allows the **Task Service** to perform all types of actions against both of the other services as long as it has a valid **OAuth2 Access Token**. ::: :::caution You have to save the create response because this is not persisted internally for security reasons. The information is necessary for the next steps. ::: ### 3. Set Up your Authorization Service In your [**Authorization Server**](/docs/authentication/unified-login/oauth2-basics#authorization-server) backend, initialize the **OAuth2Provider** recipe. Update the `supertokens.init` call to include the new recipe. ```tsx supertokens.init({ supertokens: { connectionURI: "...", apiKey: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ OAuth2Provider.init(), ] }); ``` :::caution At the moment, there is no support for creating OAuth2 providers in the Go SDK. You can use the [legacy method](/docs/microservice_auth/legacy/implementation-guide) to authenticate microservices based on your language. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import oauth2provider init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), framework="fastapi", supertokens_config=SupertokensConfig( connection_uri="...", api_key="..." ), recipe_list=[ oauth2provider.init() ], ) ``` ### 4. Generate access tokens You can directly call the [**Authorization Server**](/docs/authentication/unified-login/oauth2-basics#authorization-server) to generate Access Tokens. Check the following code snippet to see how you can do that: ```bash curl -X POST /oauth/token \ -H "Content-Type: application/json" \ -d '{ "clientId": "", "clientSecret": "", "grantType": "client_credentials", "scope": [""], "audience": "" }' ``` You should limit the scopes that you are requesting to the ones necessary to perform the desired action. :::info Custom Example If the **Task Service** wants to create an event on the **Calendar Service**, a token with the following attributes needs generation: ```json { "clientId": "", "clientSecret": "", "grantType": "client_credentials", "scope": ["event.create"], "audience": "event" } ``` ::: The **Authorization Server** returns a response that looks like this: ```json { "accessToken": "", "expiresIn": 3600 } ``` Save the `accessToken` in memory for use in the next step. The `expiresIn` field indicates how long the token is valid for. Each service that you communicate with needs its own token. With an **OAuth2 Access Token**, it can facilitate communication with the other services. Keep in mind to generate a new one when it expires. ### 5. Verify an OAuth2 Access Token To check the validity of a token, use a generic **JWT** verification library. Besides the standard **OAuth2** token claims, the implementation includes an additional one called `stt`. This stands for `SuperTokens Token Type`. It ensures that the validation occurs for the correct token type: - `0` represents a **SuperTokens Session Access Token** - `1` represents an **OAuth2 Access Token** - `2` represents an **OAuth2 ID Token**. For NodeJS you can use [`jose`](https://github.com/panva/jose) to verify the token. ```tsx const JWKS = jose.createRemoteJWKSet(new URL('/jwt/jwks.json')) async function validateClientCredentialsToken(jwt: string) { const requiredScope = ""; const audience = ''; try { const { payload } = await jose.jwtVerify(jwt, JWKS, { audience, requiredClaims: ['stt', 'scp'], }); if(payload.stt !== 1) return false; const scopes = payload.scp as string[]; return scopes.includes(requiredScope); } catch (err) { return false; } } ``` You can use the [`jwx`](https://github.com/lestrrat-go/jwx) library to verify the token. ```go return true } } return false; } ``` You can use the [PyJWT](https://github.com/jpadilla/pyjwt) library to verify the token. ```python from typing import Optional, List use Firebase\JWT\JWT; use Firebase\JWT\Key; function validateToken($jwt) { $apiDomain = ""; $apiBasePath = ""; $jwksUrl = $apiDomain . $apiBasePath . '/jwt/jwks.json'; $requiredScope = ""; $audience = ""; $jwks = json_decode(file_get_contents($jwksUrl), true); try { $decoded = JWT::decode($jwt, JWK::parseKeySet($jwks), 'RS256')); if ($decoded->aud !== $audience) { return false; } if ($decoded->sst !== 1) { return false; } return in_array($requiredScope, $decoded->scp); } catch (Exception $e) { return false; } } ``` You can use the [Auth0 JWT](https://github.com/auth0/java-jwt) library to verify the token. ```java public class JWTVerifier { private static final String JWKS_URL = "jwt/jwks.json"; private static final String AUDIENCE = ""; private static Map fetchJWKS() throws Exception { URL url = new URL(JWKS_URL); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); InputStream responseStream = connection.getInputStream(); Scanner scanner = new Scanner(responseStream, StandardCharsets.UTF_8.name()); String responseBody = scanner.useDelimiter("\\A").next(); scanner.close(); return JWT.decode(responseBody).getHeader(); } public static boolean validateToken(String token) { try { Map jwks = fetchJWKS(); Algorithm algorithm = Algorithm.RSA256(jwks.get("x5c"), null); JWTVerifier verifier = JWT.require(algorithm) .withAudience(AUDIENCE) .build(); DecodedJWT jwt = verifier.verify(token); if(jwt.getClaim("sst").asInt() != 1) { return false; } List scopes = jwt.getClaim("scp").asList(); return scopes.contains(requiredScope); } catch (Exception e) { return false; } } } ``` You can use the [IdentityModel](https://github.com/IdentityModel/IdentityModel) library to verify the token. ```csharp using System; using System.Linq; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Net.Http; using System.Threading.Tasks; using Newtonsoft.Json.Linq; class ClientCredentialsTokenValidator { static async Task ValidateToken(string jwtStr) { string apiDomain = ""; string apiBasePath = ""; string audience = ""; string requiredScope = ""; HttpClient client = new HttpClient(); var response = await client.GetStringAsync($"//jwt/jwks.json"); var jwks = new JsonWebKeySet(response); var tokenHandler = new JwtSecurityTokenHandler(); var validationParameters = new TokenValidationParameters { ValidAudience = audience, IssuerSigningKeys = jwks.Keys }; try { SecurityToken validatedToken; var principal = tokenHandler.ValidateToken(jwtStr, validationParameters, out validatedToken); var claims = principal.Claims.ToDictionary(c => c.Type, c => c.Value); if (!claims.ContainsKey("stt") || claims["stt"] != "1") { return false; } var scopes = claims["scp"].Split(" "); if (!scopes.Contains(requiredScope)) { return false; } return true; } catch (Exception) { return false; } } } ``` :::info Custom Example If the **Task Service** uses the previously generated token to create a calendar event, the **Calendar Service** needs to check the following: - Set the `stt` claim to `1` - The `scp` claim contains `event.create` - Set the `aud` claim to `event` ::: #### Handle both SuperTokens session tokens and OAuth2 access tokens If you are using your **Authorization Service** also as a **Resource Server**, account for this in the way you verify the sessions. This is necessary because two types of tokens are in use: - **SuperTokens Session Access Token**: Used during the login/logout flows. - **OAuth2 Access Token**: Used to access protected resources and perform actions that need authorization. Hence, a way to distinguish between these two and prevent errors is necessary. ```tsx async function verifySession(req: Request, res: Response, next: NextFunction) { let session = undefined; try { session = await Session.getSession(req, res, { sessionRequired: false }); } catch (err) { if ( !Session.Error.isErrorFromSuperTokens(err) || err.type !== Session.Error.TRY_REFRESH_TOKEN ) { return next(err); } } // In this case we are dealing with a SuperTokens Session that has been validated if (session !== undefined) { return next(); } // The OAuth2 Access Token needs to be manually extracted and validated let jwt: string | undefined = undefined; if (req.headers["authorization"]) { jwt = req.headers["authorization"].split("Bearer ")[1]; } if (jwt === undefined) { return next(new Error("No JWT found in the request")); } try { await validateToken(jwt); return next(); } catch (err) { return next(err); } } const JWKS = jose.createRemoteJWKSet( new URL("jwt/jwks.json"), ); // This is a basic example on how to validate an OAuth2 Token // Use the previous example to extend it async function validateToken(jwt: string) { const { payload } = await jose.jwtVerify(jwt, JWKS, { requiredClaims: ["stt", "scp", "sub"], }); if (payload.stt !== 1) throw new Error("Invalid token"); // If the Authorizaton Server will handle different types of Authorization Flows // You can differentiate between the different types of tokens by checking the `sessionHandle` claim const sessionHandle = payload['sessionHandle'] as string | undefined; if(sessionHandle === undefined) { // We are dealing with a Client Credentials Token // You can perform microservice authentication checks here } else { // Here we are validating tokens that have been generated in the Authorization Code Flow } } // You can then use the function as a middleware for a protected route const app = express(); app.get("/protected", verifySession, async (req, res) => { // Custom logic }); ``` ```python from supertokens_python.recipe.session.syncio import get_session from supertokens_python.recipe.session.exceptions import SuperTokensSessionError, TryRefreshTokenError from fastapi.requests import Request from typing import List, Optional return True def validate_token(token: str, required_scope: str) -> bool: api_domain = "" api_base_path = "/auth" client_id = "" jwks_url = f"{api_domain}{api_base_path}jwt/jwks.json" jwks_client = PyJWKClient(jwks_url) try: signing_key = jwks_client.get_signing_key_from_jwt(token) decoded = jwt.decode( token, signing_key.key, algorithms=['RS256'], options={"require": ["stt", "client_id", "scp"]} ) stt: Optional[int] = decoded.get('stt') if stt != 1: return False token_client_id: Optional[str] = decoded.get('client_id', None) if client_id != token_client_id: return False scopes: List[str] = decoded.get('scp', []) if required_scope not in scopes: return False return True except Exception: return False # ``` :::caution At the moment, there is no support for creating OAuth2 providers in the Go SDK. ::: # Authentication - Machine to Machine - Legacy flow Source: https://supertokens.com/docs/authentication/m2m/legacy-flow ## Overview This custom flow makes use of the **SuperTokens Core** without adhering to the **OAuth2** standard. It involves creating private access tokens and passing them to the microservices. The authentication sequence works in the following way: ## Microservice `M1` requests a JWT (JSON Web Token) from the **SuperTokens Core** ## Microservice `M1` sends the JWT to `M2` ## Microservice `M2` verifies the JWT and completes the action if the JWT is valid Microservice auth flow diagram The first step is to create a JWT from the microservice that sends the request (refer to this microservice as `M1`). Other microservices verify this JWT when `M1` sends them a request. Since this JWT remains static per microservice, the best time to create this is on process starts - that is when `M1` starts. The JWT can contain any information you like. At a minimum, it needs to contain information proving that it is a microservice allowed to query other microservices in your infrastructure. This is necessary since you may issue a JWT to an end user as well, and they should not be able to query any microservice directly. Add the following claim in the JWT to "mark" the JWT as one meant for microservice auth only: ```json {..., "source": "microservice", ...} ``` In the receiving microservice (`M2`), verify the JWT and check that this claim is present before serving the request. ### Security considerations #### Who can query the microservices? Anyone or any service that has direct access to the SuperTokens core can produce a valid JWT and query your microservices. If the core is open to the internet, you *must* add an API key to protect it. Even though end users may receive a JWT for their session (that the core signs), they cannot query a microservice directly. Their JWT *should* not have the `source: "microservice"` claim in it. #### What happens if someone compromises the core's API key? Then the attacker can issue their own JWTs to be able to query your microservices. To limit this protection, you may want to add firewall rules to allow access to the core only from services on your backend. You can also provide multiple API keys to the core and give a unique key to each microservice in your infrastructure. This way, it would be easier to track where a leak came from. #### What happens if someone compromises the JWT signing key? In this case, the attacker could fabricate their own JWT to be able to query your microservices. To limit this risk, a JWT signing key rotation methodology is in place. Until then, you can limit the reachability of your microservices based on the request's IP address. #### How to limit which microservice can query another one? If an organisation has multiple teams and microservices, it is common to limit which other services a given microservice can query. For example, if there exists `M1`, `M2` and `M3` microservices, there may be a situation in which `M1` should only be able to query `M2` and not `M3`. With one SuperTokens core deployment, having this type of restriction is impossible. All the microservices create and verify their JWTs using the same public/private keys. Therefore `M3` receives a request, it has no way of reliably knowing if the request is from `M1` or `M2` (assuming that IP-based access control is not implemented). This type of restriction can occur by deploying multiple cores connected to their own databases. In this example, a dedicated SuperTokens core can handle `M3`'s auth, such that only `M3` uses that to verify the incoming JWTs. Then, only other services that have access to that core can create JWTs that `M3` accepts. If `M1` doesn't have access to `M3`'s core's API key, it can be assured that successful requests to `M3` are not from `M1`. ## Steps ### 1. Create a JWT First, initialize the `JWT` recipe in the `supertokens.init` function call: ```tsx supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", // location of the core apiKey: "..." // provide the core's API key if configured }, recipeList: [ // highlight-next-line jwt.init() ] }) ``` ```go async function createJWT(payload: any) { let jwtResponse = await jwt.createJWT({ ...payload, source: "microservice" }); if (jwtResponse.status === "OK") { // Send JWT as Authorization header to M2 return jwtResponse.jwt; } throw new Error("Unable to create JWT. Should never come here.") } ``` ```go --data-raw '{ "payload": { "source": "microservice", ... }, "useStaticSigningKey": true, "algorithm": "RS256", "jwksDomain": "${apiDomain}", "validity": ${validityInSeconds} }' ``` - The value of `${connectionURI}` is the core's location - `${APIKey}` is the API key to query the core. This is only needed if an API key exists on the core. - The value of `${apiDomain}` is the domain on which the JWKs URLs are available from - `${validityInSeconds}` is the lifetime of the JWT An example response is as follows: ```json { "status": "OK", "jwt": "eyJraWQiOiI0YTE...rCFPcIRgzu_bChIIpFdA" } ``` ### 2. Store the JWT Once you create the JWT, you can store it in a (globally accessible) variable and access it when you want to talk to a microservice. Add the JWT as an `Authorization: Bearer` token like this: ```bash curl --location --request POST 'https://microservice_location/path' \ --header 'Authorization: Bearer eyJraWQiOiI0YTE...rCFPcIRgzu_bChIIpFdA' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "request": "payload" }' ``` ### 3. Verify JWTs When a target microservice receives a JWT, it must first verify it before proceeding to serve the request. The process involves two steps: - A standard verification of the JWT - Checking the JWT claim to make sure that another microservice has queried it. #### Using JWKS endpoint ##### Get JWKS endpoint The JWKS endpoint is `//jwt/jwks.json`. Here the `apiDomain` and `apiBasePath` are values pointing to the server in which you have initialized SuperTokens using the backend SDK. ##### Verify the JWT Some libraries let you provide a JWKS endpoint to verify a JWT. For example for NodeJS you can use `jsonwebtoken` and `jwks-rsa` together to achieve this. ```ts var client = jwksClient({ jwksUri: '/jwt/jwks.json' }); function getKey(header: JwtHeader, callback: SigningKeyCallback) { client.getSigningKey(header.kid, function (err, key) { var signingKey = key!.getPublicKey(); callback(err, signingKey); }); } let jwt = "..."; JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) { let decodedJWT = decoded; // Use JWT }); ``` Refer to this [GitHub gist](https://gist.github.com/rishabhpoddar/ea31502923ec9a53136371f2b6317ffa) for a code reference of how use `PyJWK` to do JWT verification. The gist contains two files: - `jwt_verification.py` (which you can copy/paste into your application). You have to modify the `JWKS_URI` in this file to point to your SuperTokens core instance (replacing the `try.supertokens.com` part of the URL). This file is for `sync` python apps, and you can modify it to work with `async` apps as well. - This file essentially exposes a function called `verify_jwt` which takes an input JWT string. - This function takes care of caching public keys in memory + auto `refetching` if the public keys have changed (which happens automatically every 24 hours with SuperTokens). This does not cause any user logouts, and is just a security feature. - `views.py`: This is an example `GET` API which extracts the JWT token from the authorization header in the request and calls the `verify_jwt` function from the other file. Refer to this [GitHub gist](https://gist.github.com/rishabhpoddar/8c26ed237add1a5b86481e72032abf8d) for a code reference of how use the Golang `jwt` lib to do session verification. The gist contains two files: - `verifyToken.go` (which you can copy/paste into your application). You have to modify the `coreUrl` in this file to point to your SuperTokens core instance (replacing the `try.supertokens.com` part of the URL). - This file essentially exposes a function called `GetJWKS` which returns a reference to the JWKS public keys useful for JWT verification. - This function takes care of caching public keys in memory + auto `refetching` if the public keys have changed (which happens automatically every 24 hours with SuperTokens). This does not cause any user logouts, and is just a security feature. - `main.go`: This is an example of how to verify a JWT using the golang JWT verification lib along with a helper function to get the JWKs keys. #### Using public key string Some JWT verification libraries require you to provide the JWT secret / public key for verification. You can obtain the JWT secret from SuperTokens in the following way: - First, query the `JWKS.json` endpoint: ```bash curl --location --request GET '/jwt/jwks.json' { "keys": [ { "kty": "RSA", "kid": "s-2de612a5-a5ba-413e-9216-4c43e2e78c86", "n": "AMZruthvYz7Ft-Dp0BC_SEEJaWK91s_YA-RR81iLJ6BTT6gJp0CcV4DfBynFU_59dRGOZyVQpAW6Drnc_6LyZpVWHROzqt-Fjh8TAqodayhPJVuZt25eQiYrqcaK_dnuHrm8qwUq-hko6q1o9NIIZWNfUBEVWmNhyAJFk5bi3pLwtKPYrUQzVLcTdDUe4SIltvvfpYHbVFnYtxkBVmqO68j7sI8ktmTXM_heals-W6WmozabDkC9_ITCeRat2f7A2l0t4QzO0ZCzZcJfhusF4X1niKgY6yYXpbX6is4HCfhYfdabcE52xYMNl-gw9XDjsIxfBMUDvOFRHWlx0rU8c=", "e": "AQAB", "alg": "RS256", "use": "sig" }, { "kty": "RSA", "kid": "d-230...802340", "n": "AMZruthvYz7...lx0rU8c=", "e": "...", "alg": "RS256", "use": "sig" } ] } ``` :::important The above shows an example output which returns two keys. There could be more keys returned based on the configured key rotation setting in the core. If you notice, each key's `kid` starts with a `s-..` or a `d-..`. The `s-..` key is a static key that never changes, whereas `d-...` keys are dynamic keys that keep changing. If you are `hardcoding` public keys somewhere, you always want to pick the `s-..` key. ::: - Next, run the NodeJS script below to convert the above output to a `PEM` file format. ```tsx // This JWK is copied from the result of the above SuperTokens core request let jwk = { "kty": "RSA", "kid": "s-2de612a5-a5ba-413e-9216-4c43e2e78c86", "n": "AMZruthvYz7Ft-Dp0BC_SEEJaWK91s_YA-RR81iLJ6BTT6gJp0CcV4DfBynFU_59dRGOZyVQpAW6Drnc_6LyZpVWHROzqt-Fjh8TAqodayhPJVuZt25eQiYrqcaK_dnuHrm8qwUq-hko6q1o9NIIZWNfUBEVWmNhyAJFk5bi3pLwtKPYrUQzVLcTdDUe4SIltvvfpYHbVFnYtxkBVmqO68j7sI8ktmTXM_heals-W6WmozabDkC9_ITCeRat2f7A2l0t4QzO0ZCzZcJfhusF4X1niKgY6yYXpbX6is4HCfhYfdabcE52xYMNl-gw9XDjsIxfBMUDvOFRHWlx0rU8c=", "e": "AQAB", "alg": "RS256", "use": "sig" }; // @ts-ignore let certString = jwkToPem(jwk); ``` The above snippet would generate the following certificate string: ```text -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxmu62G9jPsW34OnQEL9I QQlpYr3Wz9gD5FHzWIsnoFNPqAmnQJxXgN8HKcVT/n11EY5nJVCkBboOudz/ovJm ... (truncated for display) XhfWeIqBjrJheltfqKzgcJ+Fh91ptwTnbFgw2X6DD1cOOwjF8ExQO84VEdaXHStT xwIDAQAB -----END PUBLIC KEY----- ``` - You can use the generated PEM string in your code like shown below: ```ts // Truncated for display let certificate = "-----BEGIN PUBLIC KEY-----\nnMIIBIjANBgkqhki...\n-----END PUBLIC KEY-----"; let jwt = "..."; // fetch the JWT from sAccessToken cookie or Authorization Bearer header JsonWebToken.verify(jwt, certificate, function (err, decoded) { let decodedJWT = decoded; // Use JWT }); ``` :::caution Not applicable. Please use method 1 instead. ::: :::caution Not applicable. Please use method 1 instead. ::: #### Claim verification The second step is to get the JWT payload and check that it has the `"source": "microservice"` claim: ```tsx var client = jwksClient({ jwksUri: '/jwt/jwks.json' }); function getKey(header: JwtHeader, callback: SigningKeyCallback) { client.getSigningKey(header.kid, function (err, key) { var signingKey = key!.getPublicKey(); callback(err, signingKey); }); } let jwt = "..."; JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) { // highlight-start let decodedJWT = decoded; if (decodedJWT === undefined || typeof decodedJWT === "string" || decodedJWT.source === undefined || decodedJWT.source !== "microservice") { // return a 401 unauthorised error } else { // handle API request... } // highlight-end }); ``` Referring once again to this [GitHub gist](https://gist.github.com/rishabhpoddar/ea31502923ec9a53136371f2b6317ffa), in `views.py`, between lines 20 and 28, the value of a certain claim in the decoded JWT payload undergoes a check. You can do something similar and check for the `source` claim whose value must be `microservice`. If it is, then all's good, else you can return a 401. Referring once again to this [GitHub gist](https://gist.github.com/rishabhpoddar/8c26ed237add1a5b86481e72032abf8d), in `main.go`, between lines 32 and 44, the value of a certain claim in the decoded JWT payload undergoes a check. You can do something similar and check for the `source` claim whose value must be `microservice`. If it is, then all's good, else you can return a 401. #### M2M and frontend session verification for the same API You may have a setup wherein the same API receives calls from the frontend as well as from other microservices. The frontend session works differently than m2m sessions, therefore both forms of token inputs must be accounted for. The approach here would be to first attempt frontend session verification, and if that fails, then attempt m2m JWT verification (using the above method). If both fail, then send back a `401` response. The [`getSession` function](https://supertokens.com/docs/session/common-customizations/sessions/session-verification-in-api/get-session) serves for frontend session verification. ```tsx let app = express(); var client = jwksClient({ jwksUri: '/jwt/jwks.json' }); function getKey(header: JwtHeader, callback: SigningKeyCallback) { client.getSigningKey(header.kid, function (err, key) { var signingKey = key!.getPublicKey(); callback(err, signingKey); }); } app.post("/like-comment", async (req, res, next) => { // highlight-start try { let session = await Session.getSession(req, res, { sessionRequired: false }) if (session !== undefined) { // API call from the frontend and session verification is successful.. let userId = session.getUserId(); } else { // maybe this API is called from a microservice, so we attempt JWT verification as // shown above. let jwt = req.headers["authorization"]; jwt = jwt === undefined ? undefined : jwt.split('Bearer ')[1]; if (jwt === undefined) { // return a 401 unauthorised error... } else { JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) { let decodedJWT = decoded; // microservices auth is successful.. }); } } } catch (err) { next(err); } // highlight-end }); ``` - Notice that the `sessionRequired: false` option appears when calling `getSession`. This is because in case the input tokens are from another microservice, then instead of throwing an unauthorised error, the `getSession` function returns `undefined`. It's important to note that if the session does exist, but the access token has expired, the `getSession` function throws a try refresh token error, sending a 401 to the frontend. This triggers a session refresh flow as expected. - If the `getSession` function returns `undefined`, it means that the session is not from the frontend and a microservice auth verification can occur using the JWT verification method shown previously in this page. - If that fails too, send back a `401` response. ```python from typing import Optional, cast from django.http import HttpRequest from supertokens_python.recipe.session.asyncio import get_session async def like_comment(request: HttpRequest): session = await get_session(request, session_required=False) user_id = "" if session is not None: user_id = session.get_user_id() else: jwt: Optional[str] = cast(Optional[str], request.headers.get("Authorization")) # type: ignore if jwt is None: # return a 401 unauthorised error... pass else: jwt = jwt.split("Bearer ")[1] # JWT verification (see previous step) pass print(user_id) # TODO ``` - Notice that the `sessionRequired: false` option appears when calling `getSession`. This is because in case the input tokens are from another microservice, then instead of throwing an unauthorised error, the `getSession` function returns `None`. It's important to note that if the session does exist, but the access token has expired, the `getSession` function throws a try refresh token error, sending a 401 to the frontend. This triggers a session refresh flow as expected. - If the `getSession` function returns `None`, it means that the session is not from the frontend and a microservice auth verification can occur using the JWT verification method shown previously in this page. - If that fails too, send back a `401` response. ```go - If the `getSession` function returns `nil`, it means that the session is not from the frontend and a microservice auth verification can occur using the JWT verification method shown previously in this page. - If that fails too, send back a `401` response. # Authentication - Passkeys - Important concepts Source: https://supertokens.com/docs/authentication/passkeys/important-concepts ## Overview Use this page to get a high-level overview of the key concepts involved in the WebAuthn documentation. The reference goes over each term and describes how the **WebAuthn** flows work within **SuperTokens**. ## Terminology ### WebAuthn Web Authentication, **WebAuthn**, is an open web standard that enables secure, passwordless authentication for web applications. **WebAuthn** allows users to log in using biometrics, security keys, or device-based credentials, replacing traditional username and password combinations. Under the hood, the standard relies on [asymmetric (public-key) cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography) to confirm the identity of a user. For a more detailed explanation of WebAuthn, you can refer to the [actual specification](https://www.w3.org/TR/webauthn/). ### Passkeys A **passkey** is a type of credential that implements the WebAuthn standard. It uses cryptographic keys that users can share across multiple devices. This makes it convenient and recoverable in case of device loss. Aside from the ease of use, passkeys are integrated into operating systems and browsers, and, support a wide range of devices. ### Additional terms ## Credentials The technical term for the cryptographic key pairs used in **WebAuthn**. They represent the raw cryptographic material and, unlike **passkeys**, are device-specific and not synced. Additionally, sometimes, multiple credentials can be part of a single passkey. :::info Note This documentation uses **passkey** and **credential** interchangeably, even though subtle technical differences exist. ::: ## Authenticator A device or software that implements the **WebAuthn** authentication. This can be: - **Platform Authenticator**: Built-in biometric sensors like TouchID, FaceID, or Windows Hello. - **Roaming Authenticator**: External security devices like YubiKeys or Google Titan keys. ## Registration The process where a user registers their **authenticator** with your application. During this process: 1. The server generates a challenge for the **authenticator** to sign. 2. The **authenticator** creates a new credential. 3. The system saves the public key and any additional metadata for future authentication. ## Authentication The process where a user proves their identity, using their **authenticator**, by responding to a **server challenge**. Using their private key, they sign the **challenge** and then send the result to the server. The server then verifies the signature with the stored public key.
## Attestation **Attestation** represents information about the **authenticator device** itself. You can use it to verify the authenticity and the security level of the **authenticator**.
## User verification **User verification** is the method used to verify the user's presence. This can be: - Biometric verification (fingerprint, face scan). - `PIN` entry. - Physical button press on a security key.
## Authentication flows This section explains how each component communicates during different authentication flows. ### Login ## The frontend SDK requests registration options from the backend. The options are then returned based on the response from the **SuperTokens** core service. ## The **authenticator** uses the response to sign a challenge with your **passkey**. ## The result of the **authenticator** operation gets validated by the **SuperTokens** core service. ## The authentication UI updates, based on the result of the validation process. ![Sign in form UI for passkeys login](/img/webauthn-signin.png) ### Sign up ## The user enters their email address in frontend authentication UI ## The frontend SDK uses the email to request **registration** options from the backend. The options are then returned based on the response from the **SuperTokens** core service. ## The **authenticator** uses the response to sign a **challenge** with your **passkey**. ## The result of the **authenticator** operation gets validated on by the **SuperTokens** core service. ## The authentication UI updates, based on the result of the validation process. ![Passkeys sign up flow](/img/webauthn-signup.png) ### Account recovery Account recovery should use an email. In it, the user receives a link that directs them to a page where they can register a new credential. ## The frontend initiates the recovery flow by communicating with the backend SDK ## The backend checks if the email exists and then sends a recovery email. The email includes a security token obtained from the **SuperTokens** core. ## When the user accesses the recovery link, they get directed to the frontend application. The security token gets validated by the backend SDK. If successful, the SDK begins the process of registering a new credential. From here, the flow matches the one described in the previous sections. ![Sign in form UI for passkeys login](/img/webauthn-recover-account.png) # Authentication - Passkeys - Quickstart Source: https://supertokens.com/docs/authentication/passkeys/initial-setup ## Overview This page shows you how to add the **Passkeys** authentication method to your project. The tutorial creates a login flow, rendered by either the **Prebuilt UI** components or by your own **Custom UI**. ## Before you start These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). If you have skipped that page please follow the tutorial and return here once you're done. :::info SDK Support WebAuthn (Passkeys) authentication is available in the **Node.js SDK** and **Python SDK**. ::: ## Steps ### 1. Initialize the frontend SDK #### 1.1 Add the `WebAuthn` recipe in your main configuration file. ```tsx // highlight-next-line SuperTokens.init({ appInfo: { apiDomain: "...", websiteDomain: "...", appName: "...", }, recipeList: [ // highlight-start WebAuthn.init(), // highlight-end Session.init() ] }); ``` #### 1.2 Include the pre-built UI components in your application. In order for the **pre-built UI** to render inside your application, you have to specify which routes show the authentication components. The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. ```tsx // highlight-next-line class App extends React.Component { render() { return ( {/*This renders the login UI on the route*/} // highlight-next-line {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [WebauthnPreBuiltUI])} {/*Your app routes*/} ); } } ``` :::important If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample. Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details. ```tsx function AppRoutes() { const authRoutes = getSuperTokensRoutesForReactRouterDom( reactRouterDom, [/* Add your UI recipes here e.g. EmailPasswordPrebuiltUI, PasswordlessPrebuiltUI, ThirdPartyPrebuiltUI */] ); const routes = useRoutes([ ...authRoutes.map(route => route.props), // Include the rest of your app routes ]); return routes; } function App() { return ( ); } ``` ::: ```tsx // highlight-next-line class App extends React.Component { render() { // highlight-start if (canHandleRoute([WebauthnPreBuiltUI])) { // This renders the login UI on the route return getRoutingComponent([WebauthnPreBuiltUI]) } // highlight-end return ( {/*Your app*/} ); } } ``` Add the `WebAuthn` recipe in your `AuthComponent`. ```tsx title="/app/auth/auth.component.ts" @Component({ selector: "app-auth", template: '
', }) export class AuthComponent implements OnDestroy, AfterViewInit { constructor( private renderer: Renderer2, @Inject(DOCUMENT) private document: Document ) { } ngAfterViewInit() { this.loadScript('^{prebuiltUIVersion}'); } ngOnDestroy() { // Remove the script when the component is destroyed const script = this.document.getElementById('supertokens-script'); if (script) { script.remove(); } } private loadScript(src: string) { const script = this.renderer.createElement('script'); script.type = 'text/javascript'; script.src = src; script.id = 'supertokens-script'; script.onload = () => { supertokensUIInit({ appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ // highlight-start supertokensUIWebAuthn.init(), // highlight-end supertokensUISession.init(), ], }); } this.renderer.appendChild(this.document.body, script); } } ```
Add the `WebAuthn` recipe in your `AuthView` file. ```tsx ```
Call the SDK init function at the start of your application. The invocation includes the [main configuration details](/docs/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ Session.init(), WebAuthn.init(), ], }); ``` First, you need to add the recipe script tag. ```html ``` You can initialize the SDK. ```tsx supertokens.init({ appInfo: { apiDomain: "", apiBasePath: "", appName: "...", }, recipeList: [ supertokensSession.init(), supertokensWebAuthn.init(), ], }); ``` ```tsx SuperTokens.init({ apiDomain: "", apiBasePath: "", }); ``` ```kotlin void main() { SuperTokens.init( apiDomain: "", apiBasePath: "", ); } ``` ### 2. Add the passkeys UI #### 2.1 Add the sign up form Create a form in which the user can input their email address. When the user submits the form, call the `registerCredentialWithSignUp` method like in the next code snippet. Under the hood, the method communicates with the backend SDK to fetch the registration options. Once the backend responds, it uses the browser's APIs to begin the registration process. For a more detailed overview of the sign up flow check the [Important Concepts page](/docs/authentication/passkeys/important-concepts#signup). ```ts async function signUp(email: string) { try { let response = await registerCredentialWithSignUp({ email }); if ( response.status === "SIGN_UP_NOT_ALLOWED" || response.status === "INVALID_AUTHENTICATOR_ERROR" ) { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason) } else if ( response.status === "INVALID_EMAIL_ERROR" || response.status === "EMAIL_ALREADY_EXISTS_ERROR" ) { window.alert("Invalid email"); } else if ( response.status === "INVALID_CREDENTIALS_ERROR" || response.status === "OPTIONS_NOT_FOUND_ERROR" || response.status === "INVALID_OPTIONS_ERROR" || response.status === "INVALID_AUTHENTICATOR_ERROR" || response.status === "EMAIL_ALREADY_EXISTS_ERROR" || response.status === "AUTHENTICATOR_ALREADY_REGISTERED" || response.status === "FAILED_TO_REGISTER_USER" || response.status === "WEBAUTHN_NOT_SUPPORTED" ) { // These errors represent various issues with the authenticator, credential or the flow itself. // These should be handled individually by you. // The user should be informed that they should retry the sign up process or get in touch with you. window.alert("Please try again"); } else { // User signed up successfully. window.alert("You have been signed up successfully"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, // or if the input email / phone number is not valid. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ## Get the email address from the user Add a form where the user can input their email address. ## Fetch the registration options from the backend SDK When the user submits the form, call the `register options` API. Save the response to use it in the next step. ```bash curl --location --request POST '/webauthn/register/options' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johndoe@gmail.com", "displayName": "John Doe" }' ``` :::caution The returned result matches the format required by the **browser's WebAuthn API**. You will have to map the properties to the correct format based on the requirements of your platform. ::: ## Register a new credential authenticator API Use the received options generate a new credential. The implementation will vary based on the platform you are using. - **React Native**: You can use the [`react-native-passkey`](https://github.com/f-23/react-native-passkey) library. - **iOS**: Use the [`Authentication Services`](https://developer.apple.com/documentation/authenticationservices) framework. - **Android**: Use the [`Android Credential Manager API`](https://developer.android.com/identity/sign-in/credential-manager). - **Flutter**: Use [platform channels](https://docs.flutter.dev/platform-integration/platform-channels#architecture) to access the native APIs. ## Call the sign up API Using the newly generate credential, call the sign up API to save the new authentication method. ```bash curl --location --request POST '/webauthn/signup' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "webauthnGeneratedOptionsId": "opt_123...", "credential": { "id": "credential_id", "rawId": "raw_credential_id", "response": { "clientDataJSON": "base64_client_data_json", "attestationObject": "base64_attestation_object" }, "type": "public-key" }, "shouldTryLinkingWithSessionUser": true }' ``` #### 2.2 Add the login form Add a button that can trigger the sign in flow. This is all that you need in terms of UI. When the user clicks it, call the `authenticateCredentialWithSignIn` method to handle the whole process. The function uses the backend registration options to trigger the challenge signing action through the browser API. Then, it forwards the result to the backend for validation. For a more detailed overview of the login flow check the [Important Concepts page](/docs/authentication/passkeys/important-concepts#login). ```ts async function signIn(email: string) { try { let response = await authenticateCredentialWithSignIn(); if ( response.status === "SIGN_IN_NOT_ALLOWED" ) { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason) } else if (response.status === "WEBAUTHN_NOT_SUPPORTED") { // the user's browser does not support the WebAuthn standard window.alert("Login method not supported"); } else if ( response.status === "INVALID_CREDENTIALS_ERROR" || response.status === "FAILED_TO_AUTHENTICATE_USER" ) { // These errors represent various issues with the authenticator, credential or the flow itself. // These should be handled individually by you. // The user should be informed that they should retry the sign in process or get in touch with you. window.alert("Please try again"); } else { // User signed in successfully. window.alert("You have been signed in successfully"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, // or if the input email / phone number is not valid. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ## Add a button that can trigger the sign in flow ## Get the sign in options from the backend SDK When the user taps the sign in button, call the backend API to fetch the sign in options. ```bash curl --location --request POST '/webauthn/signin/options' \ --header 'Content-Type: application/json; charset=utf-8' ``` ## Use the authenticator to sign the challenge With the received options, invoke the authenticator to sign the challenge. The implementation will vary based on the platform you are using. ## Call the sign in API Send the signed challenge to the backend for validation. ```bash curl --location --request POST '/webauthn/signin' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "webauthnGeneratedOptionsId": "opt_123...", "credential": { "id": "credential_id", "rawId": "raw_credential_id", "response": { "clientDataJSON": "base64_client_data_json", "attestationObject": "base64_attestation_object" }, "type": "public-key" }, "shouldTryLinkingWithSessionUser": true }' ``` ### 2. Initialize the backend SDK ### 3. Initialize the backend SDK Initialize the backend SDK and include the **WebAuthn** `recipe`. The init call includes [configuration details](/docs/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. The recipe exposes the required endpoints that get accessed by the frontend code, and communicates with the **SuperTokens Core** to complete the authentication flow. You can [configure different aspects](/docs/authentication/passkeys/customization) of the recipe's behavior but, for the completion of this guide, use the default values. After you confirm that the flow works as expected you can explore more advanced customisations options. ```ts supertokens.init({ // Replace this with the framework you are using framework: "express", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ WebAuthN.init(), Session.init() ] }); ``` :::caution At the moment there is no support for using passkeys authentication in the Go SDK. ::: ```python from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import session, webauthn init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io", # api_key="" ), framework='flask', # Replace this with the framework you are using recipe_list=[ webauthn.init(), session.init() ] ) ``` # Authentication - Passkeys - Customization Source: https://supertokens.com/docs/authentication/passkeys/customization ## Overview Like the other **SuperTokens** authentication recipes, you can customize the `WebAuthn` flow through different configuration options and overrides. The following page describes the options that you can change and the different scenarios enabled through customization. --- ## Backend recipe configuration ```ts supertokens.init({ framework: "express", supertokens: { // https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. connectionURI: "https://try.supertokens.com", // apiKey: , }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth" }, recipeList: [ WebAuthn.init({ getOrigin: () => { return "https://example.com"; }, getRelyingPartyId: () => { return "example.com"; }, getRelyingPartyName: () => { return "example"; }, }), Session.init() // initializes session features ] }); ``` The backend recipe accepts the following properties during initialization: | Option | Description | Default | |--------|-------------|---------| | `getRelyingPartyId` | Sets the domain name associated with the WebAuthn credentials. This helps ensure that only your domain uses the credentials. | The `apiDomain` value that you have set in `appConfig` | | `getRelyingPartyName` | Sets a human-readable name for your application. The name appears to users during the WebAuthn registration process. | The `appName` value that you have set in `appConfig` | | `getOrigin` | Configures the origin URL that WebAuthn credentials bind to. This should match your application's domain and protocol. | Origin of the request | | `emailDelivery` | Configures how the system builds and sends verification emails to users. Read the [email delivery page](/docs/platform-configuration/email-delivery) for more information. | Default email service | | `validateEmailAddress` | Adds custom validation logic for email addresses. | Basic email format validation | All the properties are optional. :::caution At the moment there is no support for using passkeys authentication in the Go SDK. ::: ```python from typing import Optional from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.framework import BaseRequest from supertokens_python.recipe import session, webauthn from supertokens_python.recipe.webauthn import WebauthnConfig from supertokens_python.types.base import UserContext async def get_origin(*, tenant_id: str, request: Optional[BaseRequest], user_context: UserContext): return "https://example.com" async def get_relying_party_id(*, tenant_id: str, request: Optional[BaseRequest], user_context: UserContext): return "example.com" async def get_relying_party_name(*, tenant_id: str, user_context: UserContext): return "example" init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. connection_uri="https://try.supertokens.com", # api_key="" ), framework='flask', # Replace this with the framework you are using recipe_list=[ webauthn.init( config=WebauthnConfig( get_origin=get_origin, get_relying_party_id=get_relying_party_id, get_relying_party_name=get_relying_party_name, ) ), session.init() # initializes session features ] ) ``` The backend recipe accepts the following properties during initialization: | Option | Description | Default | |--------|-------------|---------| | `get_relying_party_id` | Sets the domain name associated with the WebAuthn credentials. This helps ensure that only your domain uses the credentials. | The `api_domain` value that you have set in `app_config` | | `get_relying_party_name` | Sets a human-readable name for your application. The name appears to users during the WebAuthn registration process. | The `app_name` value that you have set in `app_config` | | `get_origin` | Configures the origin URL that WebAuthn credentials bind to. This should match your application's domain and protocol. | Origin of the request | | `email_delivery` | Configures how the system builds and sends verification emails to users. Read the [email delivery page](/docs/platform-configuration/email-delivery) for more information. | Default email service | | `validate_email_address` | Adds custom validation logic for email addresses. | Basic email format validation | All the properties are optional. --- ## Credential generation The client generates the credentials based on the options provided by the backend SDK. The frontend SDK uses the [`navigator.credentials.created`](https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create) function to resolve this. To change the options used to generate credentials, you need to override the `registerOptions` function. ```ts supertokens.init({ framework: "express", supertokens: { // https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. connectionURI: "https://try.supertokens.com", // apiKey: , }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth" }, recipeList: [ WebAuthn.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, registerOptions: (input) => { return originalImplementation.registerOptions({ ...input, attestation: "direct", residentKey: "required", timeout: 10 * 1000, userVerification: "required", displayName: "John Doe", supportedAlgorithms: [-257], relyingPartyId: 'example.com', relyingPartyName: 'example', origin: 'https://example.com', }); }, }; }, }, }), Session.init() // initializes session features ] }); ```
#### Input properties | Name | Type | Description | Default Value | |----------|----------|-------------|---------------| | `relyingPartyId` | `string` | The domain name of your application that the system uses for validating the credential. | Uses `getRelyingPartyId` from the recipe configuration which defaults to the `apiDomain` | | `relyingPartyName` | `string` | The human-readable name of your application. | Uses `getRelyingPartyName` from the recipe configuration which defaults to the `apiName` | | `origin` | `string` | The origin URL where the credential is generated. | Uses `getOrigin` from the recipe configuration which defaults to the origin of the request | | `timeout` | `number` | The time in milliseconds that the user has to complete the credential generation process. | `6000` | | `attestation` | `"none" \| "indirect" \| "direct" \| "enterprise"` | The amount of information about the authenticator that gets included in the attestation statement. This controls what authenticators support. | `none` | | `supportedAlgorithms` | `number[]` | The cryptographic algorithms that can generate credentials. Different authenticators support different algorithms. | `[-8, -7, -257]` | | `residentKey` | `"discouraged" \| "preferred" \| "required"` | Whether the credential gest stored on the authenticator device. | `required` | | `userVerification` | `"discouraged" \| "preferred" \| "required"` | Whether user verification (like `PIN` or biometrics) is necessary. | `preferred` | | `displayName` | `string` | The display name of the user. | The user's `email` property |
:::caution At the moment there is no support for using passkeys authentication in the Go SDK. ::: ```python from typing import List, Optional, cast from typing_extensions import Unpack from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import session, webauthn from supertokens_python.recipe.webauthn import RecipeInterface, WebauthnConfig from supertokens_python.recipe.webauthn.interfaces.recipe import ( Attestation, RegisterOptionsKwargsInput, ResidentKey, UserVerification, ) from supertokens_python.recipe.webauthn.types.config import OverrideConfig from supertokens_python.types.base import UserContext def override_webauthn_functions(original_implementation: RecipeInterface): original_register_options = original_implementation.register_options async def register_options( *, relying_party_id: str, relying_party_name: str, origin: str, resident_key: Optional[ResidentKey] = None, user_verification: Optional[UserVerification] = None, user_presence: Optional[bool] = None, attestation: Optional[Attestation] = None, supported_algorithm_ids: Optional[List[int]] = None, timeout: Optional[int] = None, tenant_id: str, user_context: UserContext, **kwargs: Unpack[RegisterOptionsKwargsInput], ): return await original_register_options( relying_party_id="example.com", relying_party_name="example", origin="https://example.com", resident_key="required", user_verification="required", user_presence=True, attestation="direct", timeout=10 * 1000, tenant_id=tenant_id, user_context=user_context, email=cast(str, kwargs.get("email")), recover_account_token=cast(str, kwargs.get("recover_account_token")), display_name="John Doe", ) original_implementation.register_options = register_options return original_implementation init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth", ), supertokens_config=SupertokensConfig( # https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. connection_uri="https://try.supertokens.com", # api_key="" ), framework="flask", # Replace this with the framework you are using recipe_list=[ webauthn.init( config=WebauthnConfig( override=OverrideConfig(functions=override_webauthn_functions) ) ), session.init(), # initializes session features ], ) ```
#### Input properties | Name | Type | Description | Default Value | |----------|----------|-------------|---------------| | `relying_party_id` | `str` | The domain name of your application that the system uses for validating the credential. | Uses `get_relying_party_id` from the recipe configuration which defaults to the `api_domain` | | `relying_party_name` | `str` | The human-readable name of your application. | Uses `get_relying_party_name` from the recipe configuration which defaults to the `app_name` | | `origin` | `str` | The origin URL where the credential is generated. | Uses `get_origin` from the recipe configuration which defaults to the origin of the request | | `timeout` | `int` | The time in milliseconds that the user has to complete the credential generation process. | `6000` | | `attestation` | `"none" \| "indirect" \| "direct" \| "enterprise"` | The amount of information about the authenticator that gets included in the attestation statement. This controls what authenticators support. | `none` | | `supported_algorithms` | `List[int]` | The cryptographic algorithms that can generate credentials. Different authenticators support different algorithms. | `[-8, -7, -257]` | | `resident_key` | `"discouraged" \| "preferred" \| "required"` | Whether the credential gest stored on the authenticator device. | `required` | | `user_verification` | `"discouraged" \| "preferred" \| "required"` | Whether user verification (like `PIN` or biometrics) is necessary. | `preferred` | | `display_name` | `str` | The display name of the user. | The user's `email` property |
--- ## Credential validation When a user attempts to login, the authenticator uses their credential to sign a challenge on the client. The frontend SDK uses the [`navigator.credentials.get`](https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get) function to resolve this. The server generates the options for signing the challenge through the backend SDK, and then sends them to the client. To change those, you need to override the `signInOptions` function. ```ts supertokens.init({ framework: "express", supertokens: { // https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. connectionURI: "https://try.supertokens.com", // apiKey: , }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth" }, recipeList: [ WebAuthn.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, signInOptions: (input) => { return originalImplementation.signInOptions({ ...input, timeout: 10 * 1000, userVerification: "required", relyingPartyId: 'example.com', origin: 'https://example.com', }); }, }; }, }, }), Session.init() // initializes session features ] }); ``` #### Input properties | Name | Type | Description | Default | |----------|----------|-------------|---------| | `relyingPartyId` | `string` | The domain name of your application that the system uses for validating the credential. | Uses `getRelyingPartyId` from the recipe configuration which defaults to the `apiDomain` | | `relyingPartyName` | `string` | The human-readable name of your application. | Uses `getRelyingPartyName` from the recipe configuration which defaults to the `apiName` | | `origin` | `string` | The origin URL where the credential is generated. | Uses `getOrigin` from the recipe configuration which defaults to the origin of the request | | `timeout` | `number` | The time in milliseconds that the user has to complete the credential validation process. | `6000` | | `userVerification` | `"discouraged" \| "preferred" \| "required"` | The parameter controls whether user verification (like `PIN` or biometrics) is necessary. | `preferred` | :::caution At the moment there is no support for using passkeys authentication in the Go SDK. ::: ```python from typing import Any from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import session, webauthn from supertokens_python.recipe.webauthn import RecipeInterface, WebauthnConfig from supertokens_python.recipe.webauthn.types.config import OverrideConfig from supertokens_python.types.base import UserContext def override_webauthn_functions(original_implementation: RecipeInterface): original_sign_in_options = original_implementation.sign_in_options async def sign_in_options( *, tenant_id: str, user_context: UserContext, **kwargs: Any ): return await original_sign_in_options( tenant_id=tenant_id, user_context=user_context, timeout=10 * 1000, user_verification="required", relying_party_id='example.com', relying_party_name='Example', origin='https://example.com', ) original_implementation.sign_in_options = sign_in_options return original_implementation init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. connection_uri="https://try.supertokens.com", # api_key="" ), framework='flask', # Replace this with the framework you are using recipe_list=[ webauthn.init( config=WebauthnConfig( override=OverrideConfig( functions=override_webauthn_functions ) ) ), session.init() # initializes session features ] ) ``` #### Input properties | Name | Type | Description | Default | |----------|----------|-------------|---------| | `relying_party_id` | `str` | The domain name of your application that the system uses for validating the credential. | Uses `get_relying_party_id` from the recipe configuration which defaults to the `api_domain` | | `relying_party_name` | `str` | The human-readable name of your application. | Uses `get_relying_party_name` from the recipe configuration which defaults to the `app_name` | | `origin` | `str` | The origin URL where the credential is generated. | Uses `get_origin` from the recipe configuration which defaults to the origin of the request | | `timeout` | `int` | The time in milliseconds that the user has to complete the credential validation process. | `6000` | | `user_verification` | `"discouraged" \| "preferred" \| "required"` | The parameter controls whether user verification (like `PIN` or biometrics) is necessary. | `preferred` | # Authentication - Authenticate MCP Servers Source: https://supertokens.com/docs/authentication/ai-authentication ## Overview This guide explains how to authenticate Model Context Protocol (MCP) Servers using **SuperTokens**. The instructions make use of the `plugins` functionality. It is a new way to abstract common functionalities into a reusable package. ## Before you start The [MCP authentication flow](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) complies with the OAuth2 specifications. This means that you will have to use the `OAuth2` recipe in your configuration which is a paid feature. The functionality is only available alongside the `node` SDK at the moment. Keep in mind that the feature is currently in beta and might be subject to breaking changes. ## Steps ### 1. Install the plugin Add the `supertokens-mcp-plugin` package to your project. ```bash npm i -s supertokens-mcp-plugin ``` ```bash yarn add supertokens-mcp-plugin ``` ```bash pnpm add supertokens-auth-react supertokens-web-js ``` ### 2. Add the MCP server Use the `SuperTokensMcpServer` class when in your implementation. The class extends the base MCP server exposed by the `@modelcontextprotocol/sdk`, and adds custom authentication logic on top of it. You can authorize the client requests in two different ways. By using the standard [claim validators](/docs/additional-verification/session-verification/claim-validation). Or you can write your own custom validation logic in the `validateTokenPayload` function. The authentication state can be accessed inside a tool call through the second function argument, `extra.authInfo`. ```ts const server = new SuperTokensMcpServer({ name: "example-mcp", version: "1.0.0", path: "/mcp", validateTokenPayload: async (_accessTokenPayload, _userContext) => { // You can check the acccess token payload for any specific values return { status: "OK", }; }, // By default, you can use calim validators to determine who can access the MCP server claimValidators: [UserRoleClaim.validators.includes("admin")], }); server.registerTool( "session-info", { inputSchema: {}, description: "Get session information", }, async (_args, extra) => { return { content: [ { type: "text", text: JSON.stringify(extra.authInfo), }, ], }; } ); ``` ### 3. Update the SDK initialization code Now that you have created your server include it in the SuperTokens SDK configuration. This way, the SDK middleware will expose your new endpoint and authenticate each request. ```ts // The server that you have previously created const server = new SuperTokensMcpServer({}); supertokens.init({ supertokens: { connectionURI: "", apiKey: "", }, appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ // Include your existing recipes here // The OAuth2Provider recipe is required for the MCP authorization process OAuth2Provider.init(), ], experimental: { plugins: [ SuperTokensMcpPlugin.init({ mcpServers: [server], }), ], }, }); ``` # Additional Verification - Session Verification - Protect API routes Source: https://supertokens.com/docs/additional-verification/session-verification/protect-api-routes ## Overview You can choose between three different methods to check for a session inside an API route handler. The easiest way to do it is to use the `Verify Session` middleware. Also, depending on your use case, you can directly fetch the session or manually verify the JWT. Check each method to see which one works for you. ## Before you start --- ## Using `Verify Session` This function acts as a middleware inside your API endpoints. Hence, it requires that your backend framework supports the concept of middlewares. Besides checking for a session, it also writes responses to the client on its own, based on the session's validity and the provided configuration. ```tsx let app = express(); // highlight-start app.post("/like-comment", verifySession(), (req: SessionRequest, res) => { let userId = req.session!.getUserId(); // highlight-end //.... }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/like-comment", method: "post", //highlight-start options: { pre: [ { method: verifySession() }, ], }, handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //highlight-end //... } }) ``` ```tsx let fastify = Fastify(); //highlight-start fastify.post("/like-comment", { preHandler: verifySession(), }, (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //highlight-end //.... }); ``` ```tsx async function likeComment(awsEvent: SessionEventV2) { let userId = awsEvent.session!.getUserId(); //.... }; //highlight-next-line exports.handler = verifySession(likeComment); ``` ```tsx let router = new KoaRouter(); //highlight-start router.post("/like-comment", verifySession(), (ctx: SessionContext, next) => { let userId = ctx.session!.getUserId(); //highlight-end //.... }); ``` ```tsx class LikeComment { //highlight-start constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/like-comment") @intercept(verifySession()) @response(200) handler() { let userId = (this.ctx as SessionContext).session!.getUserId(); //highlight-end //.... } } ``` ```tsx // highlight-start export default async function likeComment(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res ) let userId = req.session!.getUserId(); // highlight-end //.... } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } let userId = session!.getUserId(); //.... return NextResponse.json({}) }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { @Post('example') @UseGuards(new AuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Session() session: SessionContainer): Promise { //highlight-start let userId = session.getUserId(); //highlight-end //.... return true; } } ``` ```go let app = express(); app.post("/like-comment", // highlight-next-line verifySession({sessionRequired: false}), (req: SessionRequest, res) => { if (req.session !== undefined) { let userId = req.session.getUserId(); } else { // user is not logged in... } } ); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/like-comment", method: "post", options: { pre: [ { // highlight-next-line method: verifySession({ sessionRequired: false }) }, ], }, handler: async (req: SessionRequest, res) => { if (req.session !== undefined) { let userId = req.session.getUserId(); } else { // user is not logged in... } } }) ``` ```tsx let fastify = Fastify(); fastify.post("/like-comment", { // highlight-next-line preHandler: verifySession({ sessionRequired: false }), }, (req: SessionRequest, res) => { if (req.session !== undefined) { let userId = req.session.getUserId(); } else { // user is not logged in... } }); ``` ```tsx async function likeComment(awsEvent: SessionEventV2) { if (awsEvent.session !== undefined) { let userId = awsEvent.session.getUserId(); } else { // user is not logged in... } }; // highlight-next-line exports.handler = verifySession(likeComment, { sessionRequired: false }); ``` ```tsx let router = new KoaRouter(); router.post("/like-comment", // highlight-next-line verifySession({ sessionRequired: false }), (ctx: SessionContext, next) => { if (ctx.session !== undefined) { let userId = ctx.session.getUserId(); } else { // user is not logged in... } } ); ``` ```tsx class LikeComment { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/like-comment") // highlight-next-line @intercept(verifySession({ sessionRequired: false })) @response(200) handler() { let session = (this.ctx as SessionContext).session; if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } } } ``` ```tsx // highlight-start export default async function likeComment(req: any, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ sessionRequired: false })(req, res, next); }, req, res ) let session = (req as SessionRequest).session; if (session !== undefined) { let userId = session.getUserId(); // session exists } else { // session doesn't exist } // highlight-end //.... } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } if (session !== undefined) { let userId = session.getUserId(); // session exists } else { // session doesn't exist } //.... return NextResponse.json({}); }, { sessionRequired: false }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { @Post('example') @UseGuards(new OptionalAuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Session() session: SessionContainer): Promise { //highlight-start if (session !== undefined) { let userId = session.getUserId(); // session exists } else { // session doesn't exist } //highlight-end //.... return true; } } ``` ```go let app = express(); app.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), async (req: SessionRequest, res) => { // All validator checks have passed and the user is an admin. } ); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), }, ], }, handler: async (req: SessionRequest, res) => { // All validator checks have passed and the user is an admin. } }) ``` ```tsx let fastify = Fastify(); fastify.post("/update-blog", { preHandler: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), }, async (req: SessionRequest, res) => { // All validator checks have passed and the user is an admin. }); ``` ```tsx async function updateBlog(awsEvent: SessionEvent) { // All validator checks have passed and the user is an admin. }; exports.handler = verifySession(updateBlog, { overrideGlobalClaimValidators: async (globalValidators) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ]) }); ``` ```tsx let router = new KoaRouter(); router.post("/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ]) }), async (ctx: SessionContext, next) => { // All validator checks have passed and the user is an admin. }); ``` ```tsx class SetRole { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/update-blog") @intercept(verifySession({ overrideGlobalClaimValidators: async (globalValidators) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ]) })) @response(200) async handler() { // All validator checks have passed and the user is an admin. } } ``` ```tsx // highlight-start export default async function setRole(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async (globalValidators) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ]) })(req, res, next); }, req, res ) // All validator checks have passed and the user is an admin. } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } // All validator checks have passed and the user is an admin. return NextResponse.json({}) }, { // highlight-start overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } // highlight-end }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { @Post('example') @UseGuards(new AuthGuard({ overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ]) })) async postExample(@Session() session: SessionContainer): Promise { // All validator checks have passed and the user is an admin. return true; } } ``` ```go let app = express(); // highlight-start app.post("/like-comment", async (req, res, next) => { try { let session = await Session.getSession(req, res); let userId = session.getUserId(); // highlight-end //.... } catch (err) { next(err); } }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/like-comment", method: "post", //highlight-start handler: async (req, res) => { let session = await Session.getSession(req, res); let userId = session.getUserId(); //highlight-end //... } }) ``` ```tsx let fastify = Fastify(); //highlight-start fastify.post("/like-comment", async (req, res) => { let session = await Session.getSession(req, res); let userId = session.getUserId(); //highlight-end //.... }); ``` ```tsx //highlight-start async function likeComment(awsEvent: SessionEvent) { let session = await Session.getSession(awsEvent, awsEvent); let userId = session.getUserId(); //highlight-end //.... }; //highlight-next-line exports.handler = middleware(likeComment); ``` ```tsx let router = new KoaRouter(); //highlight-start router.post("/like-comment", async (ctx, next) => { let session = await Session.getSession(ctx, ctx); let userId = session.getUserId(); //highlight-end //.... }); ``` ```tsx class LikeComment { //highlight-start constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/like-comment") @response(200) async handler() { let session = await Session.getSession(this.ctx, this.ctx); let userId = session.getUserId(); //highlight-end //.... } } ``` ```tsx // highlight-start export default async function likeComment(req: SessionRequest, res: any) { let session = await superTokensNextWrapper( async (next) => { return await Session.getSession(req, res); }, req, res ) let userId = session.getUserId(); // highlight-end //.... } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withPreParsedRequestResponse(request, async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { const session = await Session.getSession(baseRequest, baseResponse); let userId = session.getUserId(); return NextResponse.json({}); }); } ``` ```tsx @Controller() export class ExampleController { @Post('example') async postExample(@Req() req: Request, @Res({passthrough: true}) res: Response): Promise { //highlight-start // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res); const userId = session.getUserId(); //highlight-end //.... return true; } } ``` ```go let app = express(); app.post("/like-comment", async (req, res, next) => { try { let session = await Session.getSession(req, res, { sessionRequired: false }) if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... } catch (err) { next(err); } }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/like-comment", method: "post", handler: async (req, res) => { let session = await Session.getSession(req, res, { sessionRequired: false }) if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //... } }) ``` ```tsx let fastify = Fastify(); fastify.post("/like-comment", async (req, res) => { let session = await Session.getSession(req, res, { sessionRequired: false }) if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... }); ``` ```tsx async function likeComment(awsEvent: SessionEvent) { let session = await Session.getSession(awsEvent, awsEvent, { sessionRequired: false }) if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... }; //highlight-next-line exports.handler = middleware(likeComment); ``` ```tsx let router = new KoaRouter(); router.post("/like-comment", async (ctx, next) => { let session = await Session.getSession(ctx, ctx, { sessionRequired: false }) if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... }); ``` ```tsx class LikeComment { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/like-comment") @response(200) async handler() { let session = await Session.getSession(this.ctx, this.ctx, { sessionRequired: false }) if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... } } ``` ```tsx export default async function likeComment(req: SessionRequest, res: any) { let session = await superTokensNextWrapper( async (next) => { return await Session.getSession(req, res, { sessionRequired: false }); }, req, res ) if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withPreParsedRequestResponse(request, async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { const session = await Session.getSession(baseRequest, baseResponse, { sessionRequired: false }); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } return NextResponse.json({}); }); } ``` ```tsx @Controller() export class ExampleController { @Post('example') async postExample(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise { //highlight-start // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res, { sessionRequired: false }) if (session !== undefined) { const userId = session.getUserId(); } else { // user is not logged in... } //highlight-end //.... return true; } } ```
```go let app = express(); // highlight-start app.post("/like-comment", async (req, res, next) => { try { let session = await Session.getSession(req, res, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ] }); let userId = session.getUserId(); // highlight-end //.... } catch (err) { next(err) } }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/like-comment", method: "post", //highlight-start handler: async (req, res) => { let session = await Session.getSession(req, res, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ] }); let userId = session.getUserId(); //highlight-end //... } }) ``` ```tsx let fastify = Fastify(); //highlight-start fastify.post("/like-comment", async (req, res) => { let session = await Session.getSession(req, res, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ] }); let userId = session.getUserId(); //highlight-end //.... }); ``` ```tsx //highlight-start async function likeComment(awsEvent: SessionEvent) { let session = await Session.getSession(awsEvent, awsEvent, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ] }); let userId = session.getUserId(); //highlight-end //.... }; //highlight-next-line exports.handler = middleware(likeComment); ``` ```tsx let router = new KoaRouter(); //highlight-start router.post("/like-comment", async (ctx, next) => { let session = await Session.getSession(ctx, ctx, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ] }); let userId = session.getUserId(); //highlight-end //.... }); ``` ```tsx class LikeComment { //highlight-start constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/like-comment") @response(200) async handler() { let session = await Session.getSession(this.ctx, this.ctx, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ] }); let userId = session.getUserId(); //highlight-end //.... } } ``` ```tsx // highlight-start export default async function likeComment(req: SessionRequest, res: any) { let session = await superTokensNextWrapper( async (next) => { return await Session.getSession(req, res, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ] }); }, req, res ) let userId = session.getUserId(); // highlight-end //.... } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withPreParsedRequestResponse(request, async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { const session = await Session.getSession(baseRequest, baseResponse, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ] }); let userId = session.getUserId(); return NextResponse.json({}); }); } ``` ```tsx @Controller() export class ExampleController { @Post('example') async postExample(@Req() req: Request, @Res({passthrough: true}) res: Response): Promise { //highlight-start // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ] }); const userId = session.getUserId(); //highlight-end //.... return true; } } ```
```go function verifySession(options?: VerifySessionOptions) { return async (req: Request, res: Response, next: NextFunction) => { try { (req as any).session = await Session.getSession(req, res, options); next(); } catch (err) { if (SuperTokensError.isErrorFromSuperTokens(err)) { if (err.type === Session.Error.TRY_REFRESH_TOKEN) { // This means that the session exists, but the access token // has expired. // You can handle this in a custom way by sending a 401. // Or you can call the errorHandler middleware as shown below } else if (err.type === Session.Error.UNAUTHORISED) { // This means that the session does not exist anymore. // You can handle this in a custom way by sending a 401. // Or you can call the errorHandler middleware as shown below } else if (err.type === Session.Error.INVALID_CLAIMS) { // The user is missing some required claim. // You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend. } // OR you can use this errorHandler which will // handle all of the above errors in the default way errorHandler()(err, req, res, (err) => { next(err) }) } else { next(err) } } }; } ``` The `errorHandler` sends a `401` reply to the frontend if the `getSession` function throws an exception indicating that the session does not exist or if the access token has expired. ```go baseRequest = FlaskRequest(request) try: session = get_session( baseRequest, session_required, anti_csrf_check, check_database, override_global_claim_validators, ) except Exception as e: if isinstance(e, TryRefreshTokenError): # This means that the session exists, but the access token # has expired. # You can handle this in a custom way by sending a 401. # Or you can call the errorHandler middleware as shown below pass if isinstance(e, UnauthorisedError): # This means that the session does not exist anymore. # You can handle this in a custom way by sending a 401. # Or you can call the errorHandler middleware as shown below pass if isinstance(e, InvalidClaimsError): # The user is missing some required claim. # You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend. pass # OR you can raise this error which will # handle all of the above errors in the default way raise e if session is None: if session_required: raise Exception("Should never come here") baseRequest.set_session_as_none() else: baseRequest.set_session(session) response = make_response(f(*args, **kwargs)) return response return cast(_T, wrapped_function) return session_verify ``` If `get_session` throws an error (in case the input access token is invalid or has expired), then the SuperTokens middleware added to your app handles that exception. It sends a `401` to the frontend.
### Get the session using the `Access Token` In the above snippets, `Get Session` requires the `request` object and, depending on your backend language and framework, may also require the `response` object. Either way, this version of `Get Session` automatically reads from the request. And automatically sets the response based on the update to the session tokens. Whilst this is convenient, sometimes, you may not have the `request` or `response` objects, or you may not want SuperTokens to set the tokens in the response automatically. In this case, you can use the `getSessionWithoutRequestResponse` function. This function works similarly to `getSession`, except that it doesn't depend on the `request` or `response` objects. It's your responsibility to provide this function the access token. You must write the update tokens to the response if the tokens update during this API call. ```tsx async function verifySession(accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions) { let session: SessionContainer | undefined; try { session = await Session.getSessionWithoutRequestResponse(accessToken, antiCsrfToken, options); } catch (err) { if (SuperTokensError.isErrorFromSuperTokens(err)) { if (err.type === Session.Error.TRY_REFRESH_TOKEN) { // This means that the session exists, but the access token // has expired. // You can handle this in a custom way by sending a 401. // Or you can call the errorHandler middleware as shown below } else if (err.type === Session.Error.UNAUTHORISED) { // This means that the session does not exist anymore. // You can handle this in a custom way by sending a 401. // Or you can call the errorHandler middleware as shown below } else if (err.type === Session.Error.INVALID_CLAIMS) { // The user is missing some required claim. // You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend. } } throw err; } if (session !== undefined) { // we can use the `session` container as we usually do.. // TODO: API logic... // At the end of the API logic, we must fetch all the tokens from the session container // and set them in the response headers / cookies ourselves. const tokens = session.getAllSessionTokensDangerously(); if (tokens.accessAndFrontTokenUpdated) { // TODO: set access token in response via tokens.accessToken // TODO: set front-token in response via tokens.frontToken if (tokens.antiCsrfToken) { // TODO: set anti-csrf token update in response via tokens.antiCsrfToken } } } } ``` ```go // and set them in the response headers / cookies ourselves. tokens := session.GetAllSessionTokensDangerously() if tokens.AccessAndFrontendTokenUpdated { // TODO: set access token in response via tokens.accessToken // TODO: set front-token in response via tokens.frontToken if tokens.AntiCsrfToken != nil { // TODO: set anti-csrf token update in response via *tokens.AntiCsrfToken } } } return nil } ``` ```python from typing import Any, Callable, Dict, List, Optional, TypeVar from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( InvalidClaimsError, TryRefreshTokenError, UnauthorisedError, ) from supertokens_python.recipe.session.interfaces import SessionClaimValidator from supertokens_python.recipe.session.syncio import ( get_session_without_request_response, ) from supertokens_python.types import MaybeAwaitable _T = TypeVar("_T", bound=Callable[..., Any]) def verify_session( access_token: str, anti_csrf_token: Optional[str], anti_csrf_check: Optional[bool], session_required: Optional[bool], check_database: Optional[bool], override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]], ] ] = None, ): try: session = get_session_without_request_response( access_token, anti_csrf_token, anti_csrf_check, session_required, check_database, override_global_claim_validators, ) except Exception as e: if isinstance(e, TryRefreshTokenError): # This means that the session exists, but the access token # has expired. # You can handle this in a custom way by sending a 401. # Or you can call the errorHandler middleware as shown below pass if isinstance(e, UnauthorisedError): # This means that the session does not exist anymore. # You can handle this in a custom way by sending a 401. # Or you can call the errorHandler middleware as shown below pass if isinstance(e, InvalidClaimsError): # The user is missing some required claim. # You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend. pass # OR you can raise this error which will # handle all of the above errors in the default way raise e if session is not None: # we can use the `session` container as we usually do.. # TODO: API logic... # At the end of the API logic, we must fetch all the tokens from the session container # and set them in the response headers / cookies ourselves. tokens = session.get_all_session_tokens_dangerously() if tokens["accessAndFrontTokenUpdated"]: # TODO: set access token in response via tokens["accessToken"] # TODO: set front-token in response via tokens["frontToken"] if tokens["antiCsrfToken"] is not None: # TODO: set anti-csrf token update in response via tokens["antiCsrfToken"] pass ``` --- ## Using a JWT verification library If the previous methods are not suitable for your use case, you can use validate the token manually. This method works for cases like: - Your APIs are on a backend for which SuperTokens doesn't have SDKs. - You are using a non `http` protocol (like `websockets`) and passing in the access token. - You are using an API gateway which does JWT verification based on the JWKs endpoint. :::caution The downside to using JWT verification manually is that: - Pick and configure a JWT verification library for your framework. Many online guides explain how to do this. - You need to manually verify some custom claims in the JWT (like the user's role is `admin`, or that the user's email is verified) based on your authorization rules. - You don't have access to the [`session` object](/docs/additional-verification/session-verification/protect-api-routes#using-verify-session) using which you can modify the session's access token payload, or revoke the session. These operations can occur in an offline manner, but they reflect in the user's session only after a session refresh. ::: The manual session verification method should work in this way: ## Verify the JWT signature and expiry using a JWT verification library ## Check for custom claim values for authorization. ## Prevent cross-site request forgery (CSRF) attacks in case you are using cookies to store the JWT. ### With the JSON Web Key Set (JWKS) Endpoint Some libraries let you provide a JSON Web Key Set (JWKS) endpoint to verify a JWT. The JSON Web Key Set (JWKS) endpoint exposed by SuperTokens is available at the following URL: ```bash curl --location --request GET '/jwt/jwks.json' ``` Below is an example for NodeJS showing how you can use `jsonwebtoken` and `jwks-rsa` together to achieve JWT verification using the `jwks.json` endpoint. ```ts var client = jwksClient({ jwksUri: '/jwt/jwks.json' }); function getKey(header: JwtHeader, callback: SigningKeyCallback) { client.getSigningKey(header.kid, function (err, key) { var signingKey = key!.getPublicKey(); callback(err, signingKey); }); } let jwt = "..."; // fetch the JWT from sAccessToken cookie or Authorization Bearer header JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) { let decodedJWT = decoded; // Use JWT }); ``` Refer to this [GitHub gist](https://gist.github.com/rishabhpoddar/ea31502923ec9a53136371f2b6317ffa) for a code reference of how use `PyJWK` to do session verification. The gist contains two files: - `jwt_verification.py` (which you can copy/paste into your application). You need to modify the `JWKS_URI` in this file to point to your SuperTokens core instance (replacing the `try.supertokens.com` part of the URL). This file is for `sync` python apps and can be modified to work with `async` apps as well. - This file essentially exposes a function called `verify_jwt` which takes an input JWT string. - This function takes care of caching public keys in memory and auto re-fetching if the public keys have changed (which happens automatically every 24 hours with SuperTokens). This does not cause any user logouts and is a security feature. - `views.py`: This is an example `GET` API which extracts the JWT token from the authorization header in the request and calls the `verify_jwt` function from the other file. If you are using cookie based auth instead of header based auth, you should read the JWT from the `sAccessToken` cookie in the request. Refer to this [GitHub gist](https://gist.github.com/rishabhpoddar/8c26ed237add1a5b86481e72032abf8d) for a code reference of how use the Golang `jwt` lib to do session verification. The gist contains two files: - `verifyToken.go` (which you can copy/paste into your application). You need to modify the `coreUrl` in this file to point to your SuperTokens core instance (replacing the `try.supertokens.com` part of the URL). - This file essentially exposes a function called `GetJWKS` which returns a reference to the JSON Web Key Set (JWKS) public keys usable for JWT verification. - This function takes care of caching public keys in memory and auto re-fetching if the public keys have changed (which happens automatically every 24 hours with SuperTokens). This does not cause any user logouts and is a security feature. - `main.go`: This is an example of how to verify a JWT using the golang JWT verification lib along with the helper function to get the JWKs keys. If you are using header-based auth, you can fetch the JWT from the `Authorization Bearer` header, otherwise for cookie-based auth, you can fetch it from the `sAccessToken` cookie. Refer to this [GitHub gist](https://gist.github.com/rishabhpoddar/5b2d19c02337ed7ee387723c84def9cd) for a code reference of how use the Java `nimbus-jose-jwt` lib to do session verification. The gist contains three files: - `JWTVerification.java` You need to modify the `CORE_URL` in this file to point to your SuperTokens core instance (replacing the `try.supertokens.com` part of the URL). - This is an example of how to verify a JWT using the Java `nimbus-jose-jwt` lib along with the helper method to get the JWKs keys. If you are using header-based auth, you can fetch the JWT from the `Authorization Bearer` header, otherwise for cookie-based auth, you can fetch it from the `sAccessToken` cookie. - This file has a method called `setSource` which returns a reference to the JSON Web Key Set (JWKS) public keys usable for JWT verification. This method takes care of caching public keys in memory and auto re-fetching if the public keys have changed (which happens automatically every 24 hours with SuperTokens). This does not cause any user logouts and is a security feature. - `pom.xml`: This shows the version of `nimbus-jose-jwt` used for this project. - `InvalidClaimsException.java`: This holds the custom Exception thrown when someone has an invalid JWT body, hasn't verified their email, or hasn't set up MFA. ### With the public key string :::caution This method is less secure compared to the first method because it disables key rotation of the access token signing key. In this case, if the private key is somehow stolen, it can be used indefinitely to forge access tokens (Unless you manually change the key in the database). ::: Some JWT verification libraries require you to provide the JWT secret / public key for verification. You can obtain the JWT secret from SuperTokens in the following way: ## Query the `JWKS.json` endpoint:
```bash curl --location --request GET '/jwt/jwks.json' { "keys": [ { "kty": "RSA", "kid": "s-2de612a5-a5ba-413e-9216-4c43e2e78c86", "n": "AMZruthvYz7Ft-Dp0BC_SEEJaWK91s_YA-RR81iLJ6BTT6gJp0CcV4DfBynFU_59dRGOZyVQpAW6Drnc_6LyZpVWHROzqt-Fjh8TAqodayhPJVuZt25eQiYrqcaK_dnuHrm8qwUq-hko6q1o1o9NIIZWNfUBEVWmNhyAJFk5bi3pLwtKPYrUQzVLcTdDUe4SIltvvfpYHbVFnYtxkBVmqO68j7sI8ktmTXM_heals-W6WmozabDkC9_ITCeRat2f7A2l0t4QzO0ZCzZcJfhusF4X1niKgY6yYXpbX6is4HCfhYfdabcE52xYMNl-gw9XDjsIxfBMUDvOFRHWlx0rU8c=", "e": "AQAB", "alg": "RS256", "use": "sig" }, { "kty": "RSA", "kid": "d-230...802340", "n": "AMZruthvYz7...lx0rU8c=", "e": "...", "alg": "RS256", "use": "sig" } ] } ``` :::important The above shows an example output which returns two keys. More keys could return based on the configured key rotation setting in the core. If you notice, each key's `kid` starts with a `s-..` or a `d-..`. The `s-..` key is a static key that never changes, whereas `d-...` keys are dynamic keys that keep changing. If you are hard-coding public keys somewhere, you always want to pick the `s-..` key. One exception is that if you see a key with `kid` that doesn't start with `s-` or with `d-`, then treat that as a static key. This only happens if you used to run an older SuperTokens core that was less than version `5.0`. ::: ## Run the NodeJS script below to convert the above output to a `PEM` file format.
```tsx // This JWK is copied from the result of the above SuperTokens core request let jwk = { "kty": "RSA", "kid": "s-2de612a5-a5ba-413e-9216-4c43e2e78c86", "n": "AMZruthvYz7Ft-Dp0BC_SEEJaWK91s_YA-RR81iLJ6BTT6gJp0CcV4DfBynFU_59dRGOZyVQpAW6Drnc_6LyZpVWHROzqt-Fjh8TAqodayhPJVuZt25eQiYrqcaK_dnuHrm8qwUq-hko6q1o1o9NIIZWNfUBEVWmNhyAJFk5bi3pLwtKPYrUQzVLcTdDUe4SIltvvfpYHbVFnYtxkBVmqO68j7sI8ktmTXM_heals-W6WmozabDkC9_ITCeRat2f7A2l0t4QzO0ZCzZcJfhusF4X1niKgY6yYXpbX6is4HCfhYfdabcE52xYMNl-gw9XDjsIxfBMUDvOFRHWlx0rU8c=", "e": "AQAB", "alg": "RS256", "use": "sig" }; // @ts-ignore let certString = jwkToPem(jwk); ``` The above snippet would generate the following certificate string: ```text -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxmu62G9jPsW34OnQEL9I QQlpYr3Wz9gD5FHzWIsnoFNPqAmnQJxXgN8HKcVT/n11EY5nJVCkBboOudz/ovJm ... (truncated for display) XhfWeIqBjrJheltfqKzgcJ+Fh91ptwTnbFgw2X6DD1cOOwjF8ExQO84VEdaXHStT xwIDAQAB -----END PUBLIC KEY----- ``` Use the generated Privacy-Enhanced Mail (PEM) string in your code as shown below: ```ts // Truncated for display let certificate = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxmu62G9jPsW34OnQEL9IQQlpYr3Wz9gD5FHzWIsnoFNPqAmnQJxXgN8HKcVT/n11EY5nJVCkBboOudz/ovJm...XhfWeIqBjrJheltfqKzgcJ+Fh91ptwTnbFgw2X6DD1cOOwjF8ExQO84VEdaXHStTxwIDAQAB\n-----END PUBLIC KEY-----"; let jwt = "..."; // fetch the JWT from sAccessToken cookie or Authorization Bearer header JsonWebToken.verify(jwt, certificate, function (err, decoded) { let decodedJWT = decoded; // Use JWT }); ``` ## Tell SuperTokens to always only use the static key when creating a new session. You can accomplish this by setting the below configuration in the backend SDK: ```tsx SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ //highlight-next-line useDynamicAccessTokenSigningKey: false, }) ] }); ``` :::caution Updating this value causes a spike in the session refresh API, as and when users visit your application. :::
:::caution Not applicable. Please use method 1 instead. ::: :::caution Not applicable. Please use method 1 instead. ::: :::caution Not applicable. Please use method 1 instead. :::
### Check for custom claim values for authorization Once you have verified the access token, you can fetch the payload and perform authorization checks based on the values of the custom claims. For example, if you want to check if the user's email is verified, you should check the `st-ev` claim in the payload as shown below: ```ts var client = jwksClient({ jwksUri: '/jwt/jwks.json' }); function getKey(header: JwtHeader, callback: SigningKeyCallback) { client.getSigningKey(header.kid, function (err, key) { var signingKey = key!.getPublicKey(); callback(err, signingKey); }); } let jwt = "..."; // fetch the JWT from sAccessToken cookie or Authorization Bearer header JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) { if (err) { // send a 401 to the frontend.. } if (decoded !== undefined && typeof decoded !== "string") { let isEmailVerified = (decoded as any)["st-ev"].v if (!isEmailVerified) { // send a 403 to the frontend.. } } }); ``` Claims like email verification and user roles claims are included in the access token by the backend SDK automatically. You can even [add your own custom claims](/docs/additional-verification/session-verification/claim-validation#using-the-access-token-payload) to the access token payload, and those claims will be in the JWT as expected. :::important On claim validation failure, you must send a `403` to the frontend, which causes the frontend SDK (pre-built UI SDK) to recheck the claims added on the frontend and navigate to the right screen. ::: Referring once again to this [GitHub gist](https://gist.github.com/rishabhpoddar/ea31502923ec9a53136371f2b6317ffa), in `views.py`, between lines 20 and 28, the `st-ev` claim in the JWT payload undergoes verification. If the claim is not present or has a value of `false`, a `403` is sent to the frontend, causing the frontend SDK (pre-built UI SDK) to recheck the claims added on the frontend and navigate to the right screen. Referring once again to this [GitHub gist](https://gist.github.com/rishabhpoddar/8c26ed237add1a5b86481e72032abf8d), in `main.go`, between lines 32 and 44, the `st-ev` claim in the JWT payload undergoes verification. If the claim is not present or has a value of `false`, a `403` is sent to the frontend, causing the frontend SDK (pre-built UI SDK) to recheck the claims added on the frontend and navigate to the right screen. Referring once again to this [GitHub gist](https://gist.github.com/rishabhpoddar/5b2d19c02337ed7ee387723c84def9cd), in `JWTVerification.java`, between lines 42 and 58, the `st-ev` and `st-mfa` claims in the JWT payload undergo verification. If the claims are not present or have a value of `false`, a `403` is sent to the frontend, causing the frontend SDK (pre-built UI SDK) to recheck the claims added on the frontend and navigate to the right screen. ### Check for anti-csrf during authorization :::important You need to check for anti-cross-site request forgery (CSRF) for **non** GET requests when cookie-based authentication is active. ::: Two methods exist for configuring [cross-site request forgery (CSRF) protection](/docs/post-authentication/session-management/security#anti-csrf): `VIA_CUSTOM_HEADER` and `VIA_TOKEN`. #### `VIA_CUSTOM_HEADER` `VIA_CUSTOM_HEADER` is automatically set if `sameSite` is `none` or if your `apiDomain` and `websiteDomain` do not share the same top level domain. In this case, you need to check for the presence of the `rid` header from incoming requests. #### `VIA_TOKEN` When configured with `VIA_TOKEN`, an explicit `anti-csrf` token attaches as a header to requests with `anti-csrf` as the key. To verify the `anti-csrf` token, you need to compare it to the value of the `antiCsrfToken` key from the payload of the decoded JWT. --- # Additional Verification - Session Verification - Protect frontend routes Source: https://supertokens.com/docs/additional-verification/session-verification/protect-frontend-routes Protect frontend routes by requiring user sessions and verifying session claims for access control. ## Before you start ---
## Protect a route You can wrap your components with the `` react component. This ensures that your component renders only if the user has logged in. If they are not logged in, the user gets redirected to the login page. ```tsx // highlight-next-line // @ts-ignore class App extends React.Component { render() { return ( {/*Components that require to be protected by authentication*/} // highlight-end } /> ); } } ``` ### Optional session requirement You can provide the `requireAuth={false}` prop when using `` as shown below: ```tsx class App extends React.Component { render() { return ( // highlight-end } /> ); } } // highlight-start function MyDashboardComponent(props: any) { let sessionContext = Session.useSessionContext(); if (sessionContext.loading) { return null; } if (sessionContext.doesSessionExist) { // TODO: } else { // TODO: } return null; } // highlight-end ``` You can use the `doesSessionExist` function to check if a session exists in all your routes. ```tsx async function doesSessionExist() { if (await Session.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ## Check the claims of a session Sometimes, you may also want to check if there are certain claims in the session before granting access to a route. For example, you may want to check that the session has the admin role claim for certain APIs, or that the user has completed 2FA. You can achieve this using the session claims validator feature. Let's take an example of using the user roles claim to check if the session has the admin claim: ```tsx const AdminRoute = (props: React.PropsWithChildren) => { return ( [ ...globalValidators, UserRoleClaim.validators.includes("admin"), ] }> {props.children} ); } ``` Above, you create a generic component called `AdminRoute`, which enforces that its child components render only if the user has the admin role. In the `AdminRoute` component, the `SessionAuth` wrapper ensures that the session exists. The `UserRoleClaim` validator is also added to the `` component, which checks if the validators pass or not. If all validation passes, the `props.children` component renders. If the claim validation has failed, it displays the `AccessDeniedScreen` component instead of rendering the children. You can also pass your own custom component to the `accessDeniedScreen` prop. :::note You can extend the `AdminRoute` component to check for other types of validators as well. You can then reuse this component to protect all your app's components (In this case, you may want to rename this component to something more appropriate, like `ProtectedRoute`). ::: If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: ```tsx function ProtectedComponent() { let claimValue = Session.useClaimValue(UserRoleClaim) if (claimValue.loading || !claimValue.doesSessionExist) { return null; } let roles = claimValue.value; if (Array.isArray(roles) && roles.includes("admin")) { // User is an admin } else { // User doesn't have any roles, or is not an admin.. } } ``` ```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { // highlight-start let validationErrors = await Session.validateClaims({ overrideGlobalClaimValidators: (globalValidators) => [...globalValidators, UserRoleClaim.validators.includes("admin"), /* PermissionClaim.validators.includes("modify") */ ] }); // highlight-end if (validationErrors.length === 0) { // user is an admin return true; } for (const err of validationErrors) { if (err.id === UserRoleClaim.id) { // user roles claim check failed } else { // some other claim check failed (from the global validators list) } } } // either a session does not exist, or one of the validators failed. // so we do not allow access to this page. return false } ``` - We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. - The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that you have verified the user's email (if enabled by you). - We can also add a `PermissionClaim` validator to enforce a permission. If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: ```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { // highlight-start let roles = await Session.getClaimValue({claim: UserRoleClaim}); if (Array.isArray(roles) && roles.includes("admin")) { // User is an admin return true; } // highlight-end } // either a session does not exist, or the user is not an admin return false } ``` ## Protect a route You can use the `doesSessionExist` function to check if a session exists in all your routes. ```tsx async function doesSessionExist() { if (await Session.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ```tsx async function doesSessionExist() { if (await supertokensSession.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ```tsx async function doesSessionExist() { if (await SuperTokens.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ```kotlin if(accessTokenPayload != null) { // user is logged in } else { // user has not logged in yet } } } ``` ```swift Future doesSessionExist() async { var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); if (accessTokenPayload != null) { // user is logged in } else { // user has not logged in yet } } ``` --- ## Check the claims of a session Sometimes, you may also want to check if there are certain claims in the session before granting access to a route. For example, you may want to check that the session has the admin role claim for certain APIs, or that the user has completed 2FA. You can achieve this using the session claims validator feature. Let's take an example of using the user roles claim to check if the session has the admin claim: ```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { // highlight-start let validationErrors = await Session.validateClaims({ overrideGlobalClaimValidators: (globalValidators) => [...globalValidators, UserRoleClaim.validators.includes("admin"), /* PermissionClaim.validators.includes("modify") */ ] }); // highlight-end if (validationErrors.length === 0) { // user is an admin return true; } for (const err of validationErrors) { if (err.id === UserRoleClaim.id) { // user roles claim check failed } else { // some other claim check failed (from the global validators list) } } } // either a session does not exist, or one of the validators failed. // so we do not allow access to this page. return false } ``` - We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. - The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that you have verified the user's email (if enabled by you). - We can also add a `PermissionClaim` validator to enforce a permission. If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: ```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { // highlight-start let roles = await Session.getClaimValue({claim: UserRoleClaim}); if (roles !== undefined && roles.includes("admin")) { // User is an admin return true; } // highlight-end } // either a session does not exist, or the user is not an admin return false } ``` ```tsx async function shouldLoadRoute(): Promise { if (await supertokensSession.doesSessionExist()) { // highlight-start let validationErrors = await supertokensSession.validateClaims({ overrideGlobalClaimValidators: (globalValidators) => [...globalValidators, supertokensUserRoles.UserRoleClaim.validators.includes("admin"), /* supertokensUserRoles.PermissionClaim.validators.includes("modify") */ ] }); // highlight-end if (validationErrors.length === 0) { // user is an admin return true; } for (const err of validationErrors) { if (err.id === supertokensUserRoles.UserRoleClaim.id) { // user roles claim check failed } else { // some other claim check failed (from the global validators list) } } } // either a session does not exist, or one of the validators failed. // so we do not allow access to this page. return false } ``` - We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. - The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that you have verified the user's email (if enabled by you). - We can also add a `PermissionClaim` validator to enforce a permission. If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: ```tsx async function shouldLoadRoute(): Promise { if (await supertokensSession.doesSessionExist()) { // highlight-start let roles = await supertokensSession.getClaimValue({claim: supertokensUserRoles.UserRoleClaim}); if (roles !== undefined && roles.includes("admin")) { // User is an admin return true; } // highlight-end } // either a session does not exist, or the user is not an admin return false } ``` ```tsx async function getRole() { if (await SuperTokens.doesSessionExist()) { // highlight-start let roles: string[] = (await SuperTokens.getAccessTokenPayloadSecurely())["st-role"].v; if (roles.includes("admin")) { // TODO.. } else { // TODO.. } // highlight-end } } ``` ```kotlin val roles: List = (accessTokenPayload.get("st-role") as JSONObject).get("v") as List; if (roles.contains("admin")) { // user is an admin } else { // user is not an admin } } } ``` ```swift Future checkIfUserIsAnAdmin() async { var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); if (accessTokenPayload.containsKey("st-role")) { Map roleObject = accessTokenPayload["st-role"]; if (roleObject.containsKey("v")) { List roles = roleObject["v"]; if (roles.contains("admin")) { // user is an admin } else { // user is not an admin } } } } ``` :::tip Feature You can also [build your own custom claim validators](/docs/additional-verification/session-verification/claim-validation#using-session-claims) based on your app's requirements. ::: --- # Additional Verification - Session Verification - Session verification during server side rendering Source: https://supertokens.com/docs/additional-verification/session-verification/ssr ## Overview In Server Side Rendering (SSR) scenarios, the session verification process is slightly different. Check the following guide to understand how to adjust the flow to work with SSR. ## Before you start Getting access to the session during server side rendering is only possible using cookie-based sessions. This is the default setting, but you have to keep this in mind if you want to switch to header-based sessions. ## Steps ### 1. Enable sharing of cookies across sub domains If your API layer and website are on different sub domains (like `example.com` and `api.example.com`), then by default, the session tokens attach only to `api.example.com`. Change this to ensure that the session tokens attach to `.example.com` and the access token cookie goes to your web server on `example.com`. Enable this by [setting the `cookieDomain` configuration on the backend](/docs/post-authentication/session-management/advanced-workflows/multiple-api-endpoints). ### 2. Do JWT verification Follow the [JWT verification guide](/docs/additional-verification/session-verification/protect-api-routes#using-a-jwt-verification-library) to verify the JWT on the server side. You can read the JWT from the `sAccessToken` cookie in the request object. If the JWT is missing, invalid, or expired, redirect the user to a `/refresh-session?redirectBack=` page. You can have any path for this since it's your website. On the `/refresh-session` page, you want to call the `attemptRefreshingSession` function (from the client side). This function attempts to refresh the session. If it succeeds, it returns `true`. If it fails, it returns `false`. If it returns `true`, you want to redirect the user back to the page they were on. If it returns `false`, you want to redirect the user to the login page. ### 3. Implement the refresh session flow (`/refresh-session` page) On this path, attempt refreshing the session which can yield: - Success: The frontend gets new access and refresh tokens. Post this, redirect the user to the path mentioned on the `redirectBack` query parameter. - Failure: This would happen if the session has expired or the backend has revoked it. Either way, you want to redirect the user to the login page. Below is the code snippet that you can use on the `/refresh-session` path on the frontend ```tsx export function AttemptRefresh(props: any) { React.useEffect(() => { let cancel = false; Session.attemptRefreshingSession().then(success => { if (cancel) { // component has unmounted somehow.. return; } if (success) { // we have new session tokens, so we redirect the user back // to where they were. const urlParams = new URLSearchParams(window.location.search); window.location.href = urlParams.get('redirectBack')!; } else { // we redirect to the login page since the user // is now logged out SuperTokens.redirectToAuth(); } }) return () => { cancel = true; } }, []); return null; } ``` ```tsx function attemptRefresh() { Session.attemptRefreshingSession().then(success => { if (success) { // we have new session tokens, so we redirect the user back // to where they were. const urlParams = new URLSearchParams(window.location.search); window.location.href = urlParams.get('redirectBack')!; } else { // we redirect to the login page since the user // is now logged out window.location.href = "/login" } }) } ``` ```tsx function attemptRefresh() { Session.attemptRefreshingSession().then(success => { if (success) { // we have new session tokens, so we redirect the user back // to where they were. const urlParams = new URLSearchParams(window.location.search); window.location.href = urlParams.get('redirectBack')!; } else { // we redirect to the login page since the user // is now logged out window.location.href = "/login" } }) } ``` ```tsx function attemptRefresh() { supertokensSession.attemptRefreshingSession().then(success => { if (success) { // we have new session tokens, so we redirect the user back // to where they were. const urlParams = new URLSearchParams(window.location.search); window.location.href = urlParams.get('redirectBack')!; } else { // we redirect to the login page since the user // is now logged out window.location.href = "/login" } }) } ``` :::caution Server side rendering is not applicable for mobile apps. ::: #### Why trigger the refresh session flow instead of redirecting the user to the login page directly? Two reasons why JWT verification can fail are: - The session tokens were not passed from the frontend: This can happen if the route is accessed when the user has logged out. - The session tokens pass from the frontend, but the JWT has expired. If the user goes to the login page directly, then in the second case, the frontend redirects the user back to the current route (since the refresh token is still valid), causing an infinite loop. To counteract this issue, redirect the user to a refresh page, which creates a new access token. In the first case, when the user is actually logged out, the refreshing fails, and they go to the login page anyway. #### Can `verifySession` or `getSession` replace manual JWT verification? Yes, you can, but it is not recommended because: - Often the web server is on a different process than the API server. Using the backend SDK requires you to give it the credentials to the SuperTokens core, which you might not want to provide to the web server. - The `session` object resulting from `verifySession` and `getSession` makes it easy to mutate the access token payload. These mutations reflect on the frontend via network interceptors from the frontend SDK. In SSR, the browser makes the call to your web server directly, and the frontend SDK interceptors don't run. This can cause inconsistency between the access token payload as seen on the frontend vs the backend. --- # Additional Verification - Session Verification - WebSocket Session Verification Source: https://supertokens.com/docs/additional-verification/session-verification/with-websocket ## Overview Socket connections do not always use HTTP, which means you cannot use cookies or HTTP authorization header here. Instead, the frontend must fetch the JWT and send it at the start of the socket connection. ## Before you start ## Steps ### 1. Expose the JWT to the frontend Ensure that the JWT is available to the frontend. This is already the case in header based auth, but if you are using cookie based auth, then you should set the following boolean to true in `session.init` on the backend: ```tsx SuperTokens.init({ supertokens: { connectionURI: "..." }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ //highlight-start exposeAccessTokenToFrontendInCookieBasedAuth: true //highlight-end }) ] }); ``` ```go ```tsx declare let io: any; // REMOVE_FROM_OUTPUT async function initSocketConnection() { const token = await Session.getAccessToken(); if (token === undefined) { throw new Error("User is not logged in"); } const socket = io.connect('http://localhost:3000', { query: { token } }); return socket; } ``` - See the [docs on how to fetch the access token on the frontend for all frameworks](/docs/post-authentication/session-management/access-session-data#on-the-frontend) if needed. - The `Session.getAccessToken()` function auto refreshes the session before returning the JWT if needed. :::caution Make sure to close the socket connection whenever appropriate to avoid sending stale JWTs. ::: ### 3. Verify the JWT Verify the JWT on socket connection initialisation on the backend: ```tsx declare let io: any // REMOVE_FROM_OUTPUT // functions to fetch jwks var client = jwksClient({ jwksUri: '/jwt/jwks.json' }); function getKey(header: JwtHeader, callback: SigningKeyCallback) { client.getSigningKey(header.kid, function (err, key) { var signingKey = key!.getPublicKey(); callback(err, signingKey); }); } // socket io connection io.use(function (socket: any, next: any) { // highlight-start // we first try and verify the jwt from the token param. if (socket.handshake.query && socket.handshake.query.token) { jwt.verify(socket.handshake.query.token, getKey, {}, function (err, decoded) { if (err) return next(new Error('Authentication error')); socket.decoded = decoded; next(); }); } else { next(new Error('Authentication error')); } // highlight-end }) .on('connection', function (socket: any) { // Connection now authenticated to receive further events socket.on('message', function (message: string) { io.emit('message', message); }); }); ``` :::important Post verification, ensure that the claims in the JWT align with your authorization rules. For example, if your app requires that the user verifies their email before they use it, check that the `decoded["st-ev"].v` claim in the JWT equals `true`. Normally, the backend SDK's `getSession` or `verifySession` function performs this check for you based on your configuration. However, since JWT verification is happening manually here, you need to do those checks yourself. ::: --- # Additional Verification - Session Verification - Claim validation Source: https://supertokens.com/docs/additional-verification/session-verification/claim-validation ## Overview **SuperTokens** provides two approaches for managing access control: 1. **Session Claims**: An abstraction that includes automatic validation and refresh capabilities 2. **Access Token Payload**: A basic way to check the token payload In most of the cases the recommended way is to use session claims. You can use next table to understand the differences between the two approaches. | Feature | Session Claims | Access Token Payload | | ------------------------------------ | -------------- | -------------------- | | Store simple static data | ✅ | ✅ | | Built-in validation | ✅ | ❌ | | Automatic refresh mechanism | ✅ | ❌ | | Graceful validation failure handling | ✅ | ❌ | | Lightweight implementation | ❌ | ✅ | | No validation overhead | ❌ | ✅ | This guide shows you how to use each method. ## References ## Session Claim ### Session claim interface ```tsx type JSONObject = any; // REMOVE_FROM_OUTPUT interface SessionClaim { // Unique identifier for the claim. // For a `boolean` claim (for example if the email is verified or not), this would be a string like `"st-ev"`. readonly key: string; /** * Fetches the current value of this claim for the user. * The undefined return value signifies that we don't want to update the claim payload and or the claim value is not present in the database * This can happen for example with a second factor auth claim, where we don't want to add the claim to the session automatically. */ fetchValue( userId: string, recipeUserId: RecipeUserId, tenantId: string, currentPayload: JSONObject | undefined, // @ts-expect-error userContext: UserContext ): Promise | T | undefined; /** * Removes the claim from the payload, by cloning and updating the entire object. * * @returns The modified payload object */ // @ts-expect-error removeFromPayload(payload: JSONObject, userContext: UserContext): JSONObject; /** * Gets the value of the claim stored in the payload * * @returns Claim value */ // @ts-expect-error getValueFromPayload(payload: JSONObject, userContext: UserContext): T | undefined; } ``` The SDK provides a few base claim classes which make it easy for you to implement your own claims: - [`PrimitiveClaim`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/primitiveClaim.ts): Use this to add any primitive type value (`boolean`, `string`, `number`) to the session payload. - [`PrimitiveArrayClaim`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts): Use this to add any primitive array type value (`boolean[]`, `string[]`, `number[]`) to the session payload. - [`BooleanClaim`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/booleanClaim.ts): A special case of the `PrimitiveClaim`, used to add a `boolean` type claim. All the recipe claims are built around these primitives: - [`EmailVerificationClaim`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/emailverification/emailVerificationClaim.ts): This stores information about whether the user has verified their email. - [`RolesClaim`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/userroles/userRoleClaim.ts): This stores the list of roles associated with a user. - [`PermissionClaim`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/userroles/permissionClaim.ts): This stores the list of permissions associated with the user. #### On the frontend Like the backend, the frontend also has the concept of session claim objects which need to conform to the following interface: ```tsx type SessionClaim = { // Refresh the claim values based on an async API call refresh(): Promise; // Returns the value from the session claim getValueFromPayload(payload: any): T | undefined; // Returns the last time the claim was refreshed getLastFetchedTime(payload: any): number | undefined; }; ``` When used, these objects provide a way for the SuperTokens SDK to update the claim values when needed. For example, in the built-in email verification claim, the `refresh` function calls the backend API to check if the email has verification. That API in turn updates the session claim to reflect the email verification status. This way, even if the system marked the email as verified in offline mode, the frontend can get the email verification status update automatically. Like the backend SDK, the frontend SDK also exposes a few base claims: - [`BooleanClaim`](https://github.com/supertokens/supertokens-website/blob/master/lib/ts/claims/booleanClaim.ts) - [`PrimitiveClaim`](https://github.com/supertokens/supertokens-website/blob/master/lib/ts/claims/primitiveClaim.ts) - [`PrimitiveArrayClaim`](https://github.com/supertokens/supertokens-website/blob/master/lib/ts/claims/primitiveArrayClaim.ts) ## Claim Validator Once you add a claim to the session, specify the checks that need to run on them during session verification. For example, if an API should allow access only to `admin` roles, there must be a way to tell SuperTokens to do that check. This is where claim validators come into the picture. Here is the shape for a claim validator object: ```tsx type SessionClaim = any; // REMOVE_FROM_OUTPUT type SessionClaimValidator = { // Identifies the session claim validator // Used to know which validator failed in case multiple of them undergo checking at the same time. // The value of this is typically the same as the claim object's `key`, but you can set it to anything else. id: string; // A reference to the claim object that's associated with this validator. claim: SessionClaim; // Determines if the value of the claim should undergo fetching again. // In the built-in validators, this function typically returns `true` if the claim does not exist in the `payload`, or if it's too old. shouldRefetch: (payload: any, userContext: any) => Promise; /** extracts the claim value from the input `payload` (typically using `claim.getValueFromPayload`), and determines if the validator check has passed or not. * For example, if the validator aims to enforce that the user has verified their email, and if the claim value is `false`, then this function would return: * { * isValid: false, * reason: { * message: "wrong value", * expectedValue: true, * actualValue: false * } * } */ validate: (payload: any, userContext: any) => Promise; }; type ClaimValidationResult = { isValid: true } | { isValid: false; reason?: any }; ``` Using this interface and the claims interface, SuperTokens runs the following session claim validation process during session verification: ```tsx // @ts-nocheck function validateSessionClaims(accessToken, claimValidators) { payload = accessToken.getPayload(); // Step 1: refetch claims if required for(validator in claimValidators) { if (validator.shouldRefetch(payload)) { claimValue = validator.claim.fetchValue(accessToken.sub) payload = validator.claim.addToPayload_internal(payload, claimValue) } } failedClaims = [] // Step 2: Validate all claims for(validator in claimValidators) { validationResult = validator.validate(payload) if (!validationResult.isValid) { failedClaims.push({id: validator.id, reason: validationResult.reason}) } } return failedClaims } ``` The built-in base claims (`PrimitiveClaim`, `PrimitiveArrayClaim`, `BooleanClaim`) all expose a set of useful validators: - [`PrimitiveClaim.validators.hasValue(val, maxAgeInSeconds?)`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/primitiveClaim.ts#L50): This function call returns a validator object that enforces that the session claim has the specified `val`. - [`PrimitiveArrayClaim.validators.includes(val, maxAgeInSeconds?)`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts#L50): This checks if the the session claims value, which is an array, includes the input `val`. - [`PrimitiveArrayClaim.validators.excludes(val, maxAgeInSeconds?)`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts#L91): This checks if the the session claims value, which is an array, excludes the input `val`. - [`PrimitiveArrayClaim.validators.includesAll(val[], maxAgeInSeconds?)`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts#L136): This checks if the session claims value, which is an array, includes all the items in the input `val[]`. - [`PrimitiveArrayClaim.validators.excludesAll(val[], maxAgeInSeconds?)`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts#L178): This checks if the session claims value, which is an array, excludes all the items in the input `val[]`. In all the above claim validators, the `maxAgeInSeconds`/`maxAge` input (which is optional) governs how often to refetch the session claim value: - A value of `0` causes it to refetch the claim value each time a check happens. - If not passed, the claim is only refetched if it's missing in the session. The built-in claims like email verification or user roles claims have a default value of five minutes, meaning that those claim values refresh from the database after every five minutes. ```tsx interface SessionClaim { readonly key: string; fetchValue(userId: string, userContext: any): Promise; addToPayload(payload: any, value: T): any; getValueFromPayload(payload: any): T | undefined; } ``` ## Before you start --- ## Using session claims SuperTokens sessions have a property called `accessTokenPayload`. This is a `JSON` object which you can access on the frontend and backend. The key-values in this JSON payload refer to **claims**. ### 1. Create a custom claim ```tsx const SecondFactorClaim = new BooleanClaim({ key: "2fa-completed", fetchValue: () => false, }); ``` ### 2. Add claim validators #### Backend global validation ```tsx SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getGlobalClaimValidators: async function (input) { // @ts-expect-error return [...input.claimValidatorsAddedByOtherRecipes, SecondFactorClaim.validators.isTrue()]; }, }; }, }, }), ], }); ``` #### Backend route-specific validation ```tsx let app = express(); app.post( "/admin-only", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoleClaim.validators.includes("admin"), ], }), async (req, res) => { // Only admin users can access this endpoint }, ); ``` #### Frontend validation ```tsx const AdminRoute = (props: React.PropsWithChildren) => { return ( [ ...globalValidators, UserRoleClaim.validators.includes("admin"), ]} > {props.children} ); }; ``` ### 3. Handle validation failures #### Backend custom error handling ```tsx // @ts-expect-error if (roles === undefined || !roles.includes("admin")) { throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [ { id: UserRoleClaim.key, }, ], }); } ``` #### Frontend redirection ```tsx const AdminRoute = (props: React.PropsWithChildren) => { return ( [ ...globalValidators, { ...UserRoleClaim.validators.includes("admin"), onFailureRedirection: () => "/not-an-admin", }, ]} > {props.children} ); }; ``` --- ## Using the Access Token Payload The access token payload is a simple way to store custom data that needs to be accessible on both the frontend and the backend. ### 1. Add Custom Claims to the Access Token Payload :::important The access token payload has a set of default claims that can not be overwritten. They reserve these for standard or SuperTokens specific use-cases. Those claims are: `sub`, `iat`, `exp`, `sessionHandle`, `refreshTokenHash1`, `parentRefreshTokenHash1`, `antiCsrfToken` Trying to overwrite these values results in errors in the authentication flow process. ::: You can add custom claims to the access token payload in two ways: #### During session creation ```tsx SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, createNewSession: async function (input) { let userId = input.userId; // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, someKey: "someValue", }; return originalImplementation.createNewSession(input); }, }; }, }, }), ], }); ``` #### Post Session Creation ```tsx let app = express(); app.post("/updateinfo", verifySession(), async (req: SessionRequest, res) => { let session = req.session; await session!.mergeIntoAccessTokenPayload({ newKey: "newValue" }); res.json({ message: "successfully updated access token payload" }); }); ``` ### 2. Read the Access Token Payload #### On the backend ```tsx let app = express(); app.get("/myApi", verifySession(), async (req, res) => { let session = req.session; let accessTokenPayload = session.getAccessTokenPayload(); let customClaimValue = accessTokenPayload.customClaim; }); ``` #### On the frontend ```tsx async function someFunc() { if (await Session.doesSessionExist()) { let accessTokenPayload = await Session.getAccessTokenPayloadSecurely(); let customClaimValue = accessTokenPayload.customClaim; } } ``` --- # Additional Verification - Multi Factor Authentication - Important concepts Source: https://supertokens.com/docs/additional-verification/mfa/important-concepts ## Overview If you are new to Multi-factor authentication, MFA, this page provides a quick summary of how it works and the main terminology used in it. MFA enhances security by requiring users to authenticate through: ## **Initial login** The user enters their primary credentials, typically a username and password (first factor). ## **Authentication challenge** Upon successful entry of the primary credentials, the user receives an authentication challenge, prompting them to provide a secondary factor, such as an OTP or biometric verification. ## **Access granted** Access to the account or service is only granted when both the primary and secondary factors are successfully verified. This layered approach reduces the risk of unauthorized access, as an attacker would need to compromise multiple authentication methods to gain access. ## Terminology ### Authentication challenge An authentication challenge is a prompt that requires the user to provide additional credentials beyond the primary factor to verify their identity. This typically involves requesting a secondary factor, such as entering an OTP received via SMS/email or approving a push notification from an authenticator app. Authentication challenges are crucial in detecting and preventing unauthorized access by requiring proof of possession or identity beyond a password. ### Factors Factors refer to the different categories of credentials used in MFA: - **Something You Know**: Information only the user knows, such as a password or personal identification number (`PIN`). - **Something You Have**: A physical item the user possesses, such as a smartphone or hardware token used for generating one-time passwords (OTPs). - **Something You Are**: Biometric characteristics unique to the user, such as fingerprints, facial recognition, or voice patterns. Each auth challenge has a factor ID in SuperTokens: | Authentication Type | Factor ID | |-------------------|-----------| | Email password auth | `emailpassword` | | Social login / enterprise SSO auth | `thirdparty` | | Passwordless - Email OTP | `otp-email` | | Passwordless - SMS OTP | `otp-phone` | | Passwordless - Email magic link | `link-email` | | Passwordless - SMS magic link | `link-phone` | | TOTP | `totp` | | WebAuthn/Passkeys | `webauthn` | These factor IDs get used to configure the MFA requirements for users (except the `acccess-denied` one). They are also used to indicate which authentication challenges have completed in the current session. #### Factor completion status You can determine the status of the authentication factors by checking the session's access token payload. In the payload you can find the following claim structure: ```json { "st-mfa": { // c stands for completed, and // Shows that only the emailpassword factor has been completed "c": { // the timestamp when the factor was completed "emailpassword": 1702877939, }, // v stands for value // In this example, the false value indicates that the MFA flow is not completed // Once the second factor is finalized v will change to true "v": false } } ``` Each time an authentication factor completes, the SuperTokens backend adds it to the `c` (completed) object, and then re-evaluates the `v` boolean based on the MFA requirements for the user. ### First factor vs. secondary factor - **First Factor**: The primary authentication method, traditionally a password or `PIN`. This is the initial layer of security in the authentication process. - **Secondary Factor**: An additional security layer that complements the first factor. Common secondary factors include OTPs sent via email/SMS, authenticator apps like Google Authenticator, or biometric data. Secondary factors enhance security by adding an extra step to the verification process. A clear distinction exists between first and additional factors in the SDK. When you call `MFA.init` on the backend, you need to provide a list of allowed first factors. If using multi-tenancy, you can configure the first factors on a per-tenant basis and leave the `init` array empty. The first factors are those allowed to create a new session, whereas any other factor can only modify an existing session. In case the user calls an additional factor's API without a session, the API responds with a `401` error. ### Account linking Account linking refers to the process of connecting multiple accounts or identities across platforms or services. In the context of MFA, account linking ensures that a user's secondary authentication factors (for example, an authenticator app or backup method) are correctly associated with their primary account. This is crucial for streamlined access and maintaining secure authentication across different services without the need to set up separate MFA methods for each account. #### The relation between account linking and MFA During account linking, the individual login methods create their own "recipe user", and each recipe user links to another recipe user to create one primary user. Theoretically, one can link any recipe user to another (there is no need for them to have the same email or phone number). However, for first factor automatic linking, only link login methods if they have been verified and have the same email. From an MFA point of view, whenever the user sets up a new passwordless factor (otp-email or otp-sms), this creates a new recipe user for the passwordless recipe. It then auto-links it to the existing session's recipe user. Therefore, it is necessary to enable account linking for MFA to work. In the MFA guide, first factor account linking is not enabled, but you can enable that by following the automatic account linking guide in other parts of the docs. --- # Additional Verification - Multi Factor Authentication - Initial setup Source: https://supertokens.com/docs/additional-verification/mfa/initial-setup ## Overview To integrate multi-factor authentication, MFA, in your application, you first need to decide on what factors you want to support and when to ask for them. The following guide shows you how to implement a basic setup while also covering customization methods. ## Before you start These instructions assume that you already have some knowledge of MFA. If you are not familiar with terms like authentication factors and challenges, please go through the [MFA concepts page](/docs/additional-verification/mfa/important-concepts). If you plan to use the `otp-email` factor as a form of email verification, you also need to initialize the `emailverification` recipe in `REQUIRED` mode on the backend. This configuration ensures that the email verification process passes only if the originally provided email has been verified. ## Steps ### 1. Set up the backend #### 1.1 Enable account linking MFA requires account linking to be active. You can enable it in the following way: ```ts SuperTokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ // ... // highlight-start AccountLinking.init({ shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { if (session === undefined) { // we do not want to do first factor account linking by default. To enable that, // please see the automatic account linking docs in the recipe docs for your first factor. return { shouldAutomaticallyLink: false }; } if (user === undefined || session.getUserId() === user.id) { // if it comes here, it means that a session exists, and we are trying to link the // newAccountInfo to the session user, which means it's an MFA flow, so we enable // linking here. return { shouldAutomaticallyLink: true, shouldRequireVerification: false } } return { shouldAutomaticallyLink: false }; } }), // highlight-end ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import accountlinking from supertokens_python.types import User from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink from typing import Dict, Any, Optional, Union async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any] ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is None: # We do not want to do first factor account linking by default. # To enable that, please see the automatic account linking docs # in the recipe docs for your first factor. return ShouldNotAutomaticallyLink() if user is None or session.get_user_id() == user.id: # If it comes here, it means that a session exists, and we are trying to link the # account_info to the session user, which means it's an MFA flow, so we enable # linking here. return ShouldAutomaticallyLink(should_require_verification=False) return ShouldNotAutomaticallyLink() init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework='...', # type: ignore recipe_list=[ accountlinking.init(should_do_automatic_account_linking=should_do_automatic_account_linking) ], ) ``` - The above snippet enables auto account linking only during the second factor and not for the first factor login. This means that if a user has an email password account, and then they login via Google (with the same email), those two accounts are not linked. However, if the second factor for logging in is email or phone OTP, then that passwordless account links to the first factor login method of that session. - Notice that `shouldRequireVerification: false` configures account linking. It means that the second factor can connect to the first factor even though the first factor is not verified. If you want to do email verification of the first factor before setting up the second factor (for example if the first factor is email password, and the second is phone OTP), then you can set this boolean to `true`, and also init the email verification recipe on the frontend and backend in `REQUIRED` mode. - If you also want to enable first factor automatic account linking, see [this link](/docs/post-authentication/account-linking/automatic-account-linking). :::important Account linking is a paid feature, and you need to generate a license key to enable it. Enabling the MFA feature also enables account linking automatically, meaning you don't need to check the account linking feature. ::: #### 1.2 Configure the first factors We start by initializing the MFA recipe on the backend and specifying the list of first factors using their [factor IDs](/docs/additional-verification/mfa/important-concepts#factors). You still have to initialize all the auth recipes in the `recipeList`, and configure them based on your needs. For example, the code below initializes `thirdparty`, `emailpassword` and `passwordless` recipes and sets the `firstFactor` array to be `["emailpassword", "thirdparty"]`. This means that email password and social login appear to the user as the first factor (using the `thirdparty` + `emailpassword` recipe), and `passwordless` serves as the second factor. ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ // ... // highlight-start ThirdParty.init({ //... }), EmailPassword.init({ //... }), Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE" }), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ] }) // highlight-end ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import emailpassword, multifactorauth, thirdparty, passwordless from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework='...', # type: ignore recipe_list=[ emailpassword.init(), thirdparty.init(), multifactorauth.init(), passwordless.init(contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE"), multifactorauth.init(first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY]) ], ) ``` Other combinations of first factors exists. For example, if you want passwordless as the first factor, then you would init the passwordless recipe and add `"passwordless"` in the `firstFactors` array. For a multi-tenancy setup, where each tenant can have a different set of first factors, you can leave the `firstFactors` array as `undefined` in the `MultiFactorAuth.init`. Configure the `firstFactors` on a per-tenant basis when creating or updating a tenant as shown below: ```tsx async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD] }); if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } } ``` `firstFactors` includes only `"emailpassword"`. This means that users who login to this tenant can only use email password as the first factor. Later on, the configuration for passwordless as a second factor for this tenant appears. :::important - If you do not configure `firstFactors` array on a tenant configuration, then no factors activate for that tenant by default. - To remove the `firstFactors` configuration for a tenant, you can pass a `null` value for the `firstFactors` key in the tenant configuration. For that tenant, this makes SuperTokens default to the `firstFactors` array in the `MultiFactorAuth.init` from the backend `init` configuration. ::: :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds async def create_new_tenant(): resp = await create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate(first_factors=[FactorIds.EMAILPASSWORD]) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds def create_new_tenant(): resp = create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate(first_factors=[FactorIds.EMAILPASSWORD]) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```bash showAppTypeSelect curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data-raw '{ "tenantId": "customer1", "firstFactors": ["emailpassword"] }' ``` `firstFactors` includes only `"emailpassword"`. This means that users who login to this tenant can only use email password as the first factor. Later on, the configuration for passwordless as a second factor for this tenant appears. :::important - If you do not configure `firstFactors` array on a tenant configuration, then no factors activate for that tenant by default. - To remove the `firstFactors` configuration for a tenant, you can pass a `null` value for the `firstFactors` key in the tenant configuration. For that tenant, this makes SuperTokens default to the `firstFactors` array in the `MultiFactorAuth.init` from the backend `init` configuration. ::: Email Password enabled In the above setting, Email Password is active in the **Login Methods** section. This means that users who login to this tenant can only use email password as the first factor. Later on, the configuration for passwordless as a second factor for this tenant appears. By default, no login methods activate for a tenant. #### 1.3 Configure the second factor This section explains how to configure SuperTokens such that a second factor is necessary for all users during sign up and during sign in. TOTP serves as an example for the second factor. The following code snippet accomplishes this: ```ts SuperTokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ // ... ThirdParty.init({ //... }), EmailPassword.init({ //... }), Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE" }), totp.init(), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { return [MultiFactorAuth.FactorIds.TOTP] } } } } // highlight-end }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( emailpassword, multifactorauth, thirdparty, passwordless, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.types import User from supertokens_python.recipe.multifactorauth.types import MFARequirementList def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: return [FactorIds.TOTP] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ emailpassword.init(), thirdparty.init(), multifactorauth.init(), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" ), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` In the above snippet, you configure email password and social login as the first factor, followed by TOTP as the second factor. After sign in or sign up, SuperTokens calls the `getMFARequirementsForAuth` function to get a list of secondary factors for the user. The returned value determines the boolean value of `v` that's stored in the session's access token payload. If the returned factor is already completed (it's in the `c` object of the session's payload), then the value of `v` is `true`, else `false`. In the above example, `"totp"` returns as a required factor for all users. However, you can also dynamically decide which factor to return based on the `input` arguments, which contains the `User` object, the `tenantId`, and the current session's access token payload. The default implementation of `getMFARequirementsForAuth` returns the set of factors specifically enabled for this user (see next section) or for the tenant (see later section). The output of this function can be more complex than a `string[]`. You can also return an object which tells SuperTokens that any one of the factors must satisfy: ## Require one of multiple factors ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ // ... MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { // highlight-start return [{ oneOf: [ MultiFactorAuth.FactorIds.TOTP, MultiFactorAuth.FactorIds.OTP_EMAIL ] }] // highlight-end } } } } }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.types import User from supertokens_python.recipe.multifactorauth.types import MFARequirementList def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # highlight-next-line return [FactorIds.TOTP, FactorIds.OTP_EMAIL] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` ## Require multiple factors in any order ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ // ... MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { // highlight-start return [{ allOfInAnyOrder: [ MultiFactorAuth.FactorIds.TOTP, MultiFactorAuth.FactorIds.OTP_EMAIL ] }] // highlight-end } } } } }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.types import User from supertokens_python.recipe.multifactorauth.types import MFARequirementList def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # highlight-next-line return [{"allOfInAnyOrder": [FactorIds.TOTP, FactorIds.OTP_EMAIL]}] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` ## Require multiple factors in a specific order ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ // ... MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { // highlight-start let currentCompletedFactors = MultiFactorAuth.MultiFactorAuthClaim.getValueFromPayload(input.accessTokenPayload) if (MultiFactorAuth.FactorIds.TOTP in currentCompletedFactors.c) { // this means the totp factor is completed return [MultiFactorAuth.FactorIds.OTP_EMAIL] } else { // this means we have not finished totp yet, and we want // to do that right after first factor login return [MultiFactorAuth.FactorIds.TOTP] } // highlight-end } } } } }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.types import User from supertokens_python.recipe.multifactorauth.types import MFARequirementList from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( MultiFactorAuthClaim, ) def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # highlight-start current_completed_factors = MultiFactorAuthClaim.get_value_from_payload( access_token_payload ) if current_completed_factors and FactorIds.TOTP in current_completed_factors.c: # this means the totp factor is completed return [FactorIds.OTP_EMAIL] else: # this means we have not finished totp yet, and we want # to do that right after first factor login return [FactorIds.TOTP] # highlight-end original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ```
:::note no-title You can return an empty array from `getMFARequirementsForAuth` if you don't want any further MFA done for the current user. :::
For a multi tenant setup, you can configure a list of secondary factors when creating / modifying a tenant as shown below: ```tsx async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD], requiredSecondaryFactors: [MultiFactorAuth.FactorIds.OTP_EMAIL] }); if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } } ``` In the above code, you add a property called `requiredSecondaryFactors` for a tenant whose value is a `string[]`. You add `otp-email` as a factor ID above which means that all users who log into that tenant must complete `otp-email` as a second factor. To remove the `requiredSecondaryFactors` configuration for a tenant, you can pass a `null` value for the `requiredSecondaryFactors` key in the tenant configuration. If you add more than one item in this array, it means that the user must complete any one of factors mentioned in the array. If you want to have a different behavior for the tenant, you can achieve that by overriding the `getMFARequirementsForAuth` function as shown below: :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds async def create_new_tenant(): resp = await create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD], required_secondary_factors=[FactorIds.OTP_EMAIL], ) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds def create_new_tenant(): resp = create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD], required_secondary_factors=[FactorIds.OTP_EMAIL], ) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```bash curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data-raw '{ "tenantId": "customer1", "firstFactors": ["emailpassword"], "requiredSecondaryFactors": ["otp-email"] }' ``` In the above code, you add a property called `requiredSecondaryFactors` for a tenant whose value is a `string[]`. You add `otp-email` as a factor ID above which means that all users who log into that tenant must complete `otp-email` as a second factor. To remove the `requiredSecondaryFactors` configuration for a tenant, you can turn off all the toggles. If you add more than one item in this array, it means that the user must complete any one of factors mentioned in the array. If you want to have a different behavior for the tenant, you can achieve that by overriding the `getMFARequirementsForAuth` function as shown below: OTP - Email enabled As shown above, you turn on `OTP - Email` in the **Secondary Factors** section which means that all users who log into that tenant must complete `otp-email` as a second factor. You can also turn off all factors to have no secondary factors required for the tenant. If you turn on more than one factor, it means that the user must complete any one of factors that are active. If you want to have a different behavior for the tenant, you can achieve that by overriding the `getMFARequirementsForAuth` function as shown below: ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ // ... MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { // highlight-start return [{ allOfInAnyOrder: await input.requiredSecondaryFactorsForTenant }] // highlight-end } } } } }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.types import User from supertokens_python.recipe.multifactorauth.types import MFARequirementList def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # highlight-start return [{"allOfInAnyOrder": await required_secondary_factors_for_tenant()}] # highlight-end original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` Notice that the input to the function contains the `requiredSecondaryFactorsForTenant` array. This would be the same list that you passed to the tenant configuration when creating / modifying the tenant as shown in the previous steps. #### 1.4 Remove the second factor requirement {{optional}} Instead of configuring a factor for all users in your app, or for all users within a tenant, you may want to implement a flow in which users do MFA only if they have enabled it for themselves. Here, users may also want to choose what factors they would like to enable for themselves. This flow allows users to configure their MFA preferences in the settings page in your app's frontend. A pre-built UI for this is not yet provided, but in this section, we explain the setup on the backend. You want to start by creating an API that does [session verification](/docs/additional-verification/session-verification/protect-api-routes), and then enable the desired factor for the user. For example, if the user wants to enable TOTP, then you would call the following function in your API: ```ts async function enableMFAForUser(userId: string) { await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP) } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.multifactorauth.asyncio import add_to_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds async def enable_mfa_for_user(user_id: str): await add_to_required_secondary_factors_for_user( user_id, FactorIds.TOTP ) ``` ```python from supertokens_python.recipe.multifactorauth.syncio import add_to_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds def enable_mfa_for_user(user_id: str): add_to_required_secondary_factors_for_user( user_id, FactorIds.TOTP ) ``` The effect of the above function call is that in the default implementation of `getMFARequirementsForAuth`, the factors specifically enabled for the input user are considered. By default, if you add multiple factors for a user ID, then it would require them to complete any one of those secondary factors during login. If you want to change the default behavior from "any one of" to something else (like "all of"), you can do this by overriding the `getMFARequirementsForAuth` function: ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ // ... MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], override: { functions: (originalImplementation) => { return { ...originalImplementation, // highlight-start getMFARequirementsForAuth: async function (input) { return [{ allOfInAnyOrder: await input.requiredSecondaryFactorsForUser }] } // highlight-end } } } }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.types import User from supertokens_python.recipe.multifactorauth.types import MFARequirementList def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # highlight-start return [{"allOfInAnyOrder": await required_secondary_factors_for_user()}] # highlight-end original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` Once you call the `addToRequiredSecondaryFactorsForUser` function for a user, SuperTokens stores this preference in the user metadata JSON of the user. For example, if you add `"totp"` as a required secondary factor for a user, this preference is stored in the metadata JSON as: ```json { "_supertokens": { "requiredSecondaryFactors": ["totp"] } } ``` You can view this JSON on the [user details page of the user management dashboard](/docs/post-authentication/dashboard/user-management) and modify it manually if you like. To know the factors that a user has enabled, you can use the following function: ```ts async function isTotpEnabledForUser(userId: string) { let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId) return factors.includes(MultiFactorAuth.FactorIds.TOTP) } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds async def is_totp_enabled_for_user(user_id: str): factors = await get_required_secondary_factors_for_user( user_id ) return FactorIds.TOTP in factors ``` ```python from supertokens_python.recipe.multifactorauth.syncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds def is_totp_enabled_for_user(user_id: str): factors = get_required_secondary_factors_for_user( user_id ) return FactorIds.TOTP in factors ``` Using the above function, you can build your settings page on the frontend which displays the existing enabled factors for the user. Allow users to enable or disable factors as they like. Once you have enabled a factor for a user, you take them to that factor setup screen if they have not previously already setup the factor. To know if a factor is setup, you can call the following function (on the backend): ```ts async function isTotpSetupForUser(userId: string) { let factors = await MultiFactorAuth.getFactorsSetupForUser(userId) return factors.includes(MultiFactorAuth.FactorIds.TOTP) } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.multifactorauth.asyncio import get_factors_setup_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds async def is_totp_enabled_for_user(user_id: str): factors = await get_factors_setup_for_user( user_id ) return FactorIds.TOTP in factors ``` ```python from supertokens_python.recipe.multifactorauth.syncio import get_factors_setup_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds def is_totp_enabled_for_user(user_id: str): factors = get_factors_setup_for_user( user_id ) return FactorIds.TOTP in factors ``` Or you can call the [`MFAInfo` endpoint](/docs/additional-verification/mfa/initial-setup#the-mfa-info-endpoint) from the frontend which returns information indicating which factors have already been setup for the user and which not. A factor is considered setup if the user has gone through that factor's flow at least once. For example, if the user has created and verified a TOTP device, only then does the `getFactorsSetupForUser` function return `totp` as part of the array. Likewise, if the user has completed `otp-email` or `link-email` once, only then do these factors become a part of the returned array. Let's take two examples: - The first time the user enables TOTP, then the result of `getFactorsSetupForUser` does not contain `"totp"`. You should redirect the user to the TOTP setup screen. Once they add and verify a device, then `getFactorsSetupForUser` returns `["totp"]` even if they later disable TOTP from the settings page and re-enable it. - Let's say that the first factor for a user is `emailpassword`, and the second factor is `otp-email`. Once they sign up, SuperTokens already knows the email for the user, when they are doing the `otp-email` step, then they are not asked to enter their email again (that is, an OTP is directly sent to them). However, until they actually complete the OTP flow, `getFactorsSetupForUser` does not return `["otp-email"]` as part of the output. :::caution In the edge case that a factor is active for a user, but they sign out before setting it up, then when they login next, SuperTokens still asks them to complete the factor at that time. If SuperTokens doesn't have the required information (like no TOTP device for TOTP auth), then users need to set up a device at that point in time. If you would like to change how this works and only want users to set up their factor via the settings page, and not during sign in, you can do this by overriding the `getMFARequirementsForAuth` function, which takes as an input the list of factors that are setup for the current user. ::: The subsequent sections in this doc walk through frontend setup, and also specific examples of common MFA flows. ### 2. Set up the frontend The pre-built UI provides support for the following MFA methods: - TOTP - Email / phone OTP If you want other types of MFA (like magic links, or password), please consider checking out the custom UI second. We start by initialising the MFA recipe on the frontend and providing the list of first factors as shown below: ```tsx supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init( /* ... */), Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", }), // highlight-start MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ] }) // highlight-end ] }) ``` You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init( /* ... */), supertokensUIPasswordless.init({ contactMethod: "EMAIL_OR_PHONE", }), // highlight-start supertokensUIMultiFactorAuth.init({ firstFactors: [ supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY ] }) // highlight-end ] }) ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ Session.init(), MultiFactorAuth.init() ], }); ``` In the above snippet, `thirdparty` and email password are configured as first factors. The second factor is determined [on the backend](/docs/additional-verification/mfa/initial-setup#1-set-up-the-backend), based on the boolean value of [`v` in the MFA claim in the session](/docs/additional-verification/mfa/important-concepts#factors). If the `v` is `false` in the session, it means that there are still factors pending before the user has completed login. In this case, the frontend SDK calls the `MFAInfo` endpoint (see more about this later) on the backend which returns the list of factors (`string[]`) that the user must complete next. For example: - If the next array is `["otp-email"]`, then the user sees the enter OTP screen for the email associated with the first factor login. - If the `n` array has multiple items, the user sees a [factor chooser screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/mfa-chooser--multiple-factors) using which they can decide which factor they want to continue with. - If the `next` is empty, it means that: - A misconfiguration exists on the backend. This would show an access denied screen to the user. OR; - Another claim needs to satisfy first (like email verification), before the next MFA challenge can display. This can happen if you configure the `backend`'s `checkAllowedToSetupFactorElseThrowInvalidClaimError` function to not allow a factor setup until the email has been verified. If you notice, in the above code snippet, `Passwordless.init` is also included, and this handles cases where the second factor is `otp-email` or `otp-sms`. For TOTP, a different recipe is used as shown later in this guide. For a multi-factor setup, the first factors are selected based on [the configuration of the tenant](./backend-setup#multi-tenant-setup). Each tenant has a `firstFactors` array configuration which determines the login options shown for that tenant. For MFA, the login options are determined by the [`requiredSecondaryFactors` configuration on the tenant](./backend-setup#multi-tenant-setup-1), or based on the customisations for `getMFARequirementsForAuth` on the backend. To tell the frontend to dynamically load the factors based on the tenant, four things need to supply: - The current `tenantId` - Enable dynamic login methods - Add `MultiFactorAuth.init` to the recipe list without any configured `firstFactors` - Init all the recipes that can be possibly used by any tenant as the first or second factor. ```tsx supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, // highlight-next-line usesDynamicLoginMethods: true, recipeList: [ Multitenancy.init({ override: { functions: (oI) => { return { ...oI, // highlight-start getTenantId: (input) => { // Implement the following based on the UX flow you want for // tenant discovery return "TODO.." } // highlight-end } } } }), EmailPassword.init( /* ... */), Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", }), // highlight-start MultiFactorAuth.init() // highlight-end ] }) ``` You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, // highlight-next-line usesDynamicLoginMethods: true, recipeList: [ supertokensUIMultitenancy.init({ override: { functions: (oI) => { return { ...oI, // highlight-start getTenantId: (input) => { // Implement the following based on the UX flow you want for // tenant discovery return "TODO.." } // highlight-end } } } }), supertokensUIEmailPassword.init( /* ... */), supertokensUIPasswordless.init({ contactMethod: "EMAIL_OR_PHONE", }), // highlight-start supertokensUIMultiFactorAuth.init() // highlight-end ] }) ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ Session.init(), MultiFactorAuth.init() ], }); ``` - In the above code snippet, `ThirdPartyEmailPassword` and `Passwordless` are included as the auth methods. This works for a variety of use cases like: - The first factor for any tenant can be third party or email password login, and the second factor can be passwordless login (`otp-email` or `otp-sms`). - The first factor for any tenant can be email password, with, or without a second factor (like `otp-email`).. - The first factor for any tenant can be third party, with, or without a second factor (like `otp-email`).. - The first factor for any tenant can be passwordless login (with magic link), with or without a second factor (like `otp-email`). - You can even change `passwordles.init` to using `thirdpartypasswordless.init` if you want to have the first factor for any tenant to be `thirdparty` or passwordless login, with or without a second factor (like `otp-email`). - The `MultiFactorAuth` is configured without any configured `firstFactors` because the frontend is set to dynamically load the first factors based on the tenant. Therefore, `usesDynamicLoginMethods: true` is included in the `SuperTokens.init` call. - The `Multitenancy` is configured as well, and a skeleton for `getTenantId` is provided. You need to implement this function based on the UX flow desired for tenant discovery. For example, [here is a common UX flow in which the tenant ID is determined based on the current sub domain](/docs/authentication/enterprise/subdomain-login). :::important - If you do initialize the `firstFactors` array for `MultiFactorAuth.init()` on the frontend, it is not considered when `usesDynamicLoginMethods: true` is included. - If the tenant doesn't have the `firstFactors` array set, then the list of first factors that appear is determined by the [login methods that are enabled in that tenant's configuration](/docs/multi-tenancy/new-tenant). ::: The second factor for a tenant is selected based on the [`secondaryFactors` configuration for the tenant](./backend-setup#multi-tenant-setup-1), or based on any custom implementation for the `getMFARequirementsForAuth` function. If the current user has specific MFA methods enabled for them, those are also shown as options as well. Overall, the list of secondary factors is used to build the `next` array returned from the `MFAInfo` endpoint (see more about this later). For example: - If the next array is `["otp-email"]`, then the user sees the enter OTP screen for the email associated with the first factor login. - If the `n` array has multiple items, the user sees a [factor chooser screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/mfa-chooser--multiple-factors) using which they can decide which factor they want to continue with. - If the `next` is empty, it means that: - A misconfiguration exists on the backend. This would show an access denied screen to the user. OR; - Another claim needs to satisfy first (like email verification), before the next MFA challenge can display. This can happen if you configure the `backend`'s `checkAllowedToSetupFactorElseThrowInvalidClaimError` function to not allow a factor setup until the email has been verified. In the subsequent sections, specific MFA setup examples are given for your reference. #### Usage with email verification If you are also requiring email verification, the user must verify the email first, and then all the MFA challenges. For example, if the user has email password as the first factor, and then TOTP as a second factor, SuperTokens prompts the user to do email password login, followed by email verification, followed by TOTP. To switch the order such that email verification happens after the secondary factors of MFA, follow the next code snippet. ```tsx supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes... EmailVerification.init({ mode: "REQUIRED", }), Session.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, // highlight-start getGlobalClaimValidators: (input) => { let emailVerificationClaimValidator = input.claimValidatorsAddedByOtherRecipes.find(v => v.id === EmailVerification.EmailVerificationClaim.id)!; let filteredValidators = input.claimValidatorsAddedByOtherRecipes.filter(v => v.id !== EmailVerification.EmailVerificationClaim.id); return [...filteredValidators, emailVerificationClaimValidator]; } // highlight-end } } } }) ] }) ``` In the snippet above, the `getGlobalClaimValidators` function in the Session recipe is overridden to add the email verification validator at the end of the returned validators array. This ensures that post the first factor sign up, the first validator that fails is the MFA one which redirects the user to complete the MFA factors. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes... supertokensUIEmailVerification.init({ mode: "REQUIRED", }), supertokensUISession.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, // highlight-start getGlobalClaimValidators: (input) => { let emailVerificationClaimValidator = input.claimValidatorsAddedByOtherRecipes.find(v => v.id === supertokensUIEmailVerification.EmailVerificationClaim.id)!; let filteredValidators = input.claimValidatorsAddedByOtherRecipes.filter(v => v.id !== supertokensUIEmailVerification.EmailVerificationClaim.id); return [...filteredValidators, emailVerificationClaimValidator]; } // highlight-end } } } }) ] }) ``` In the snippet above, the `getGlobalClaimValidators` function in the Session recipe is overridden to add the email verification validator at the end of the returned validators array. This ensures that post the first factor sign up, the first validator that fails is the MFA one which redirects the user to complete the MFA factors. #### Handle misconfigurations There can be situations of misconfigurations. For example you may have enabled `otp-email` for a user as a secondary factor, but did not add `Passwordless` (or `ThirdPartyPasswordless`) in the `recipeList` on the frontend. In such (and similar) situations, the pre-built UI on the frontend throws an error which is sent to the error boundary of your app. The way to solve these errors is to recheck the `recipeList` on the frontend, and make sure that it has all the recipes initialized that are necessary for any factor configured on the backend. #### The access denied screen Sometimes, users may end up seeing [an access denied screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/totp-mfa--device-setup-access-denied-reload) during the login flow. This appears if there is a 500 (backend sends a 500 status code) error during the MFA flow for API calls that are automatically initiated (without user action). For example: - When the user wants to setup a new `TOTP` device, the pre-built UI calls the `createDevice` function from the `totp` recipe on page load, and if that fails, users see the access denied screen asking them to retry. - When the user needs to complete an OTP email factor, and if the API call to send an email (which starts on page load) fails, then users see the access denied screen asking them to retry. You can override this component in the following way: ```tsx function App() { return ( { return (
Access denied! {props.error === undefined ? null : props.error}
); }, }}> {/* Rest of the JSX */}
); } export default App; ```
```tsx function App() { if(canHandleRoute([/*...*/])){ return ( { return (
Access denied! {props.error === undefined ? null : props.error}
); }, }}> {getRoutingComponent([/*...*/])}
) } return ( {/* Rest of the JSX */} ); } export default App; ```
:::caution You cannot override the pre-built UI in non react apps yet. :::
After the first factor sign in is over, to know the next auth challenge, the frontend should rely on the session's access token payload MFA claim's `n` array. For example, the access token payload may have the following content: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, }, "v": false } } ``` This means that the user has completed the email password login, and that there are still MFA login challenge(s) remaining (`v` is `false`). #### 1.1 Initialize the MFA recipe ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... // highlight-next-line MultiFactorAuth.init() ], }); ``` ```tsx supertokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... // highlight-next-line supertokensMultiFactorAuth.init() ], }); ``` :::success This step is not applicable for mobile apps. Please continue reading. ::: #### 1.2 Add the MFA flow The overall lifecycle of a factor post sign in is as follows: ## Asking for the first factor This is the same as setting up a recipe per the other recipe guides. Please follow those. ## Checking the `v` boolean value in the MFA claim After the first factor is complete, the frontend needs to check if there are any pending MFA challenges. This can be done by reading the `v` claim from the session as shown below: ```tsx async function isAllMFACompleted() { if (await Session.doesSessionExist()) { let mfaClaim = await Session.getClaimValue({ claim: MultiFactorAuth.MultiFactorAuthClaim }); if (mfaClaim === undefined) { // this can happen during migration where the session is an older one // that was created before MFA was introduced on the backend return true; } else { return mfaClaim.v } } else { throw new Error("Illegal function call: For first factor setup, you do not need to call this function") } } ``` ```tsx async function isAllMFACompleted() { if (await supertokensSession.doesSessionExist()) { let mfaClaim = await supertokensSession.getClaimValue({ claim: supertokensMultiFactorAuth.MultiFactorAuthClaim }); if (mfaClaim === undefined) { // this can happen during migration where the session is an older one // that was created before MFA was introduced on the backend return true; } else { return mfaClaim.v } } else { throw new Error("Illegal function call: For first factor setup, you do not need to call this function") } } ``` ```tsx async function isAllMFACompleted() { if (await SuperTokens.doesSessionExist()) { // highlight-start let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; return isMFACompleted // highlight-end } } ``` ```kotlin val isMFACompleted: Boolean = (accessTokenPayload.get("st-mfa") as JSONObject).get("v") as Boolean; return isMFACompleted; } } ``` ```swift let mfaObject: [String: Any] = accessTokenPayload["st-mfa"] as? [String: Any], // Determine if MFA has been completed let isMFACompleted: Bool = mfaObject["v"] as? Bool { // Return the MFA completion status return isMFACompleted } // Return false if any of the unwrapping fails, indicating MFA completion status cannot be confirmed return false } } ``` ```dart Future isAllMFACompleted() async { var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); if (accessTokenPayload.containsKey("st-mfa")) { Map mfaObject = accessTokenPayload["st-mfa"]; if (mfaObject.containsKey("v")) { bool isMFACompleted = mfaObject["v"]; return isMFACompleted; } } return false; // Return false if "st-mfa" is not present or "v" is not found } ``` ## Checking the `next` array Once it is verified that MFA is still pending, the list of factors the user must do next needs to be retrieved. This can be done by calling the [MFA Info endpoint](#mfa-info-endpoint) which returns a list of next (`string[]`) factors: - If there are multiple values in this array, then the frontend needs to show these options to the user and ask them to pick one of them. - If there is only one item, then the UI can directly ask the user to complete that factor. - If this array is empty, then: - A misconfiguration exists on the backend. This would show an access denied screen to the user. OR; - Another claim needs to satisfy first (like email verification), before the next MFA challenge can display. This can happen if you configure the `backend`'s `checkAllowedToSetupFactorElseThrowInvalidClaimError` function to not allow a factor setup until the email has been verified. ## Checking for factor setup Once the user has picked a specific factor (or if `next` contains only one item), you need to check if that factor has already been setup for that user. A factor is setup already if: - For `totp`: The user has already added a `totp` device and verified it. - For `otp-email`: The user has a passwordless `loginMethod` that has an email associated with it. - For `link-email`: The user has a passwordless `loginMethod` that has an email associated with it. Note that this is not a valid secondary factor, but is a valid first factor. - For `otp-sms`: The user has a passwordless `loginMethod` that has a phone number associated with it. - For `link-phone`: The user has a passwordless `loginMethod` that has a phone number associated with it. Note that this is not a valid secondary factor, but is a value first factor. - For `emailpassword`: The user has an email password `loginMethod`. - For `thirdparty`: The user has a third party `loginMethod`. If the user has the factor already setup, you can skip the setup step and directly ask them for the challenge: - For `totp`: Ask them to enter the OTP. - For `otp-email`: Send them an email with the OTP, and ask them to enter the OTP. - For `otp-sms`: Send them an SMS with the OTP, and ask them to enter the OTP. - For `emailpassword`: Ask them to enter their password. - For `thirdparty`: Ask them to login using the third party provider. In case the user does not have the factor setup, you need to ask them to set it up first: - For `totp`: Ask them to scan the QR code and enter the TOTP to verify the device. - For `otp-email`: Ask them to enter their email and send them an email with the OTP. Once they enter the OTP, a passwordless user is created and associated with their user object. Note that if you already have the user's email from another login method (see later), you do not need to ask them to enter their email again. In that way, it would be similar to as if the factor is already setup, but technically, it is not. - For `otp-sms`: Ask them to enter their phone number and send them an SMS with the OTP. Once they enter the OTP, a passwordless user is created and associated with their user object. Note that if you already have the user's phone number from another login method (see later), you do not need to ask them to enter their phone number again. In that way, it would be similar to as if the factor is already setup, but technically, it is not. - For `emailpassword`: Ask them to enter their email and password. Once they enter the password, an email password user is created and associated with their user object. Note that if you already have the user's email from another login method (see later), you do not need to ask them to enter their email again. In that way, it would be similar to as if the factor is already setup, but technically, it is not. Here you would be calling the sign up API, vs in the other case (where the factor is already setup), you would be calling the sign in API. - For `thirdparty`: Ask them to login using the third party provider. Once they login, a third party user is created and associated with their user object. In the later guides of this recipe, the use cases are described. If you want to know the status of any factor, you can get that by calling the [MFA Info endpoint](#mfa-info-endpoint). ## References ### The MFA info endpoint This is an important endpoint which can be utilized to: - Know which factors are pending for the user (referred to as the the `next` array in the documentation). - Update the `v` and `c` values in the MFA claim. - Get a list of all factors that are already setup for the session user. - For each factor, get a list of emails / phone numbers that can be utilized for that factor. Our pre-built UI uses this API automatically, but you can also always call this API manually if you are building a custom UI: ```tsx async function fetchMFAInfo() { if (await Session.doesSessionExist()) { try { let mfaInfo = await MultifactorAuth.resyncSessionAndFetchMFAInfo() let factorEmails = mfaInfo.emails; let factorPhoneNumbers = mfaInfo.phoneNumbers; let emailsForOTPEmail = factorEmails["otp-email"]; let phoneNumbersForOTPSms = factorEmails["otp-sms"]; let isTotpSetup = mfaInfo.factors.alreadySetup.includes("totp"); let isOTPEmailSetup = mfaInfo.factors.alreadySetup.includes("otp-email"); let isOTPSmsSetup = mfaInfo.factors.alreadySetup.includes("otp-sms"); let next = mfaInfo.factors.next; let factorsAllowedToBeSetup = mfaInfo.factors.allowedToSetup; } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: For first factor setup, you do not need to call this function") } } ``` ```tsx async function fetchMFAInfo() { if (await supertokensSession.doesSessionExist()) { try { let mfaInfo = await supertokensMultiFactorAuth.resyncSessionAndFetchMFAInfo() let factorEmails = mfaInfo.emails; let factorPhoneNumbers = mfaInfo.phoneNumbers; let emailsForOTPEmail = factorEmails["otp-email"]; let phoneNumbersForOTPSms = factorEmails["otp-sms"]; let isTotpSetup = mfaInfo.factors.alreadySetup.includes("totp"); let isOTPEmailSetup = mfaInfo.factors.alreadySetup.includes("otp-email"); let isOTPSmsSetup = mfaInfo.factors.alreadySetup.includes("otp-sms"); let next = mfaInfo.factors.next; let factorsAllowedToBeSetup = mfaInfo.factors.allowedToSetup; } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: For first factor setup, you do not need to call this function") } } ``` - In the above code snippet, the list of factors which the user must complete next (in the `next` array) is retrieved along with all the relevant information to know what state each factor is in to decide if the user should be prompted to setup the factor (for example create a new TOTP device), or solve the auth challenge instead (for example, showing the enter TOTP screen). - The function is called `resyncSessionAndFetchMFAInfo` because it does two things: - fetches the MFA info that you can consume to know the `next` array and what state each factor is in. - resynchronizes the value of the `v` and `c` in the session's MFA claim. Call the following API when you want to know the status of any factor. Notice that the API call requires the session's access token as an input (this should be included by the frontend SDK automatically): ```bash curl --location --request PUT '/mfa/info' \ --header 'Authorization: Bearer ...' ``` - The structure of the raw JSON response is as follows: ```json { "status": "OK", "factors": { "alreadySetup": ["totp", "otp-email", "..."], "allowedToSetup": ["otp-sms", "otp-email", "..."], "next": ["otp-sms", "..."] }, "emails": { "otp-email": ["user1@example.com", "user2@example.com"], "link-email": ["user1@example.com", "user2@example.com"], }, "phoneNumbers": { "otp-sms": ["+1234567890", "+1098765432"], "link-phone": ["+1234567890", "+1098765432"], }, } ``` - `factors.alreadySetup` is an array that contains all factors that have been setup by the user. If the current factor is a part of this array, it means that you can directly take the user to the factor challenge screen. If your factor depends on an email or phone number (like in the case of `otp-sms` or `otp-email`), then you can find the email to send the code to in the `emails` or `phoneNumbers` object in the response with the key as the current factor ID. - `factors.allowedToSetup` is an array that contains all factors that the user can setup at this point. This is not that useful during the sign in process, but may be useful post sign in if you want to know what are the factors that the user can setup at any point in time. - `emails` is an object in which the key are all the factor IDs supported by SuperTokens (and any custom factor ID added by you). The values against each of the keys is a list of emails that can be utilized to complete the factor. The first email (index 0) in the list is the preferred email to use for the factor. The order is determined based on the first factor chosen by the user, and if the factor was already setup or not. If the array is empty, it means that there is no email associated with the user for that factor. This can happen only if the factor was not already setup. In this case, you should take the user to a screen to ask them to first enter an email, and then to the challenge screen. The flow is further explained in the common flows guide later on. - `phoneNumbers` is similar to the `emails` object, except that it contains phone numbers for factors that are dependent on phone numbers. - The `factors.next` array determines the list of factors which the user must completed next. For example: - If the next array is `["otp-email"]`, then the user sees the enter OTP screen for the email associated with the first factor login. - If the `n` array has multiple items: - For the pre-built UI, the user sees a [factor chooser screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/mfa-chooser--multiple-factors) using which they can decide which factor they want to continue with. - For custom UI, you would need to make this screen on your own. - If the `next` is empty, it means that: - A misconfiguration exists on the backend. This would show an access denied screen to the user. OR; - Another claim needs to satisfy first (like email verification), before the next MFA challenge can display. This can happen if you configure the `checkAllowedToSetupFactorElseThrowInvalidClaimError` function, on the backend, to not allow a factor setup until the email is verified. ### Handle support cases Some situations exist in which users may be locked out of their accounts and would need you to do certain steps to unlock their accounts. These cases are: ## `ERR_CODE_009` - This can happen when the second factor is `emailpassword`: - API Path is `/signin POST`. - Output JSON: ```json { "status": "SIGN_IN_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_009)" } ``` - This can happen if the email password account you are trying to do MFA with is not verified. ## `ERR_CODE_010` - This can happen when the second factor is `emailpassword`: - API Path is `/signin POST`. - Output JSON: ```json { "status": "SIGN_IN_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_010)" } ``` - This can happen if the email password account you are trying to do MFA with is already linked to another primary user that is not equal to the session user. ## `ERR_CODE_011` - This can happen when the second factor is `emailpassword`: - API Path is `/signin POST`. - Output JSON: ```json { "status": "SIGN_IN_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_011)" } ``` - This can happen if the email password account you are trying to do MFA cannot link to the session user because there already exists another primary user with the same email. ## `ERR_CODE_012` - This can happen when the second factor is `emailpassword`: - API Path is `/signin POST`. - Output JSON: ```json { "status": "SIGN_IN_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_012)" } ``` - To link the email password user with the session user, it must be confirmed that the session user is a primary user. However, that can fail if there exists another primary user with the same email as the session user, and in this case, this error is sent to the frontend. ## `ERR_CODE_013` - This can happen when the second factor is `emailpassword`: - API Path is `/signup POST`. - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_013)" } ``` - An example scenario of when in the following scenario: - A user signs up with their phone number and OTP - Post sign up, they are prompted to add their email and a password for the account. In this case, since the entered email is not verified, this error will display. - To resolve this, it is advised to change the flow to first ask the user to go through the email OTP flow post the first factor sign up, and then add a password to the account. This way, the email will be verified. ## `ERR_CODE_014` - This can happen when the second factor is `emailpassword`: - API Path is `/signup POST`. - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_014)" } ``` - An example scenario of when in the following scenario: - Let's say that the app is set up to not have automatic account linking during the first factor. - A user creates an email password account with email `e1`, verifies it, and links social login account to it with email `e2`. - The user logs out, and then creates a social login account with email `e1`. Then, they are prompted to add a password to this account. Since an email password account with `e1` already exists, SuperTokens will try and link that to this new account, but fail, since the email password account with `e1` is already a primary user. - To resolve this, it is advised to manually link the `e1` social login account with the `e1` email password account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. ## `ERR_CODE_015` - This can happen when the second factor is `emailpassword`: - API Path is `/signup POST`. - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_015)" } ``` - An example scenario of when in the following scenario: - A user creates a social login account with email `e1` which becomes a primary user. - The user logs out, and creates another social login account with email `e2`, which also becomes a primary user. - The user is prompted to add a password for the new account with an option to also specify an email with it (this is strange, but theoretically possible). They now enter the email `e1` for the email password account. - This will cause this type of error since the linking of the new social login and email account will fail since there already exists another primary user with the same (`e1`) email. - To resolve this, it is advised not allowing users to specify an email when asking them to add a password for their account. ## `ERR_CODE_016` - This can happen when the second factor is `emailpassword`: - API Path is `/signup POST`. - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_016)" } ``` - An example scenario of when in the following scenario: - Let's say that the app is set up to not have automatic account linking during the first factor. - A user signs up with a social login account using Google with email `e1`, and they add another social account, with Facebook, with the same email. - The user logs out and creates another social login account with email `e1` (say `GitHub`), and then tries and adds a password to this account with email `e1`. Here, SuperTokens will try and make the `GitHub` login a primary user, but fail, since the email `e1` is already a primary user (with Google login). - To resolve this, it is advised to manually link the `e1` `GitHub` social login account with the `e1` Google social login account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. ## `ERR_CODE_017` - This can happen when the second factor relies on the passwordless recipe. - API Path is `/signinup/code/consume POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_017)" } ``` - This can happen when the passwordless account is trying to link to the account of the first factor, but it can't because the passwordless account is already linked with another primary user. ## `ERR_CODE_018` - This can happen when the second factor relies on the passwordless recipe. - API Path is `/signinup/code/consume POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_018)" } ``` - This can happen when the passwordless account is trying to link to the account of the first factor, but it can't because there exists another primary user with the same email as the passwordless account. ## `ERR_CODE_019` - This can happen when the second factor relies on the passwordless recipe. - API Path is `/signinup/code POST` or `/signinup/code/consume POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_019)" } ``` - This can happen when the passwordless account is trying to link to the account of the first factor, but, the first factor account cannot become a primary user because there exists another account with the same email as the first factor user account which is already primary. If you are using otp-email MFA factor as a form of email verification, you should also have `emailverification` recipe initialised in `REQUIRED` mode on the backend (no need to add it on the frontend since users won't see that UI). This is for security reasons wherein during the sign up process, when asking for the otp-email challenge, the email the OTP is sent to is determined on the frontend (automatically). In this case, the following scenario is possible: - User signs up with email `A` - An email OTP challenge is displayed to the user, and an OTP to email `A` is sent automatically. - The user manually calls the OTP create code API with email `B` and their session token, and verifies the OTP via a call to the consume code API. - The user refreshes the page and the otp-email challenge is complete. Of course, this is not the desired flow when you want to use otp-email as a form of email verification. To prevent this, you should have the `emailverification` recipe initialised in `REQUIRED` mode on the backend. This ensures that the email verification claim validator only passes if the email that's verified is the one from the first factor (email `A`). The above case is only possible during sign up, and not sign in. ::: ### Security considerations SuperTokens enforces that a user has completed all the required factors by keeping track of and checking them in the user's access token payload. - If a user is required to complete a MFA challenge, for example TOTP, if they already have a verified TOTP device, they cannot setup any other factor before completing this factor challenge, and if they do not yet have a verified TOTP device, then the only action they are allowed to take is to create a new TOTP device. This ensures that a user cannot bypass the MFA challenges of the current or future step. - When a user creates a new TOTP device, it cannot be utilized unless they first verify it by entering the initial TOTP code. - If the email of the 2nd factor login method is not confirmed, by default, it is not allowed to be setup or used as a 2nd factor, unless the session user has a login method that has the same email which is verified. - A fixed number of times (5 times by default) a user can enter an invalid TOTP code, after which they have to wait for 15 minutes before trying again. This timeout and the max attempts count can be modified in the core configuration. - During sign up (not sign in), for email / SMS OTP challenge, the email / SMS that the OTP is sent to is determined by the frontend. This is intentional because it allows you to create a flow in which the email the OTP is sent to may not be the same as the login method of the first factor. However, from a security point of view, it allows a malicious actor to send an OTP to a different email / phone number than the first factor's phone or email. This is not an issue if you are using email OTP as a method for email verification because the email verification recipe checks that the email of the first factor is verified, and in the case of the malicious user, the email of the first factor won't be verified because they entered a different email for otp-email challenge. --- # Additional Verification - Multi Factor Authentication - TOTP - TOTP required for all users Source: https://supertokens.com/docs/additional-verification/mfa/totp/totp-for-all-users ## Overview This guide shows you how to implement an MFA policy that requires all users to use TOTP before they get access to your application. ## Before you start The tutorial assumes that the first factor is email password or social login, but the same set of steps are applicable for other first factor types. ## Steps ### 1. Configure the backend To start with, we configure the backend in the following way: ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), // highlight-next-line totp.init(), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { return [MultiFactorAuth.FactorIds.TOTP] } } } } // highlight-end }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, MFARequirementList, ) from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # Get roles for the user return [FactorIds.TOTP] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ totp.init(), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` - Notice that we have initialised the TOTP recipe in the `recipeList`. By default, no configs are required for it, but you can provide: - `issuer`: This is the name that will show up in the TOTP app for the user. By default, this is equal to the `appName` config, however, you can change it to something else using this property. - `defaultSkew`: The default value of this is `1`, which means that TOTP codes that were generated 1 tick before, and that will be generated 1 tick after from the current tick will be accepted at any given time (including the TOTP of the current tick, of course). - `defaultPeriod`: The default value of this is `30`, which means that the current tick is value for 30 seconds. So by default, a TOTP code that's just shown to the user, is valid for 60 seconds (`defaultPeriod + defaultSkew*defaultPeriod` seconds) - We also override the `getMFARequirementsForAuth` function to indicate that `totp` must be completed before the user can access the app. Notice that we do not check for the userId there, and return `totp` for all users. Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished `totp`, the payload will look like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "totp": 1702877999 }, "v": true } } ``` Indicating that the user has finished all required factors, and should be allowed to access the app. ### 2. Configure the frontend We start by modifying the `init` function call on the frontend like so: ```tsx supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes.. // highlight-start totp.init(), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ] }) // highlight-end ] }) ``` You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: This change is in your auth route config. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes.. // highlight-start supertokensUITOTP.init(), supertokensUIMultiFactorAuth.init({ firstFactors: [ supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY ] }) // highlight-end ] }) ``` This change goes in the `supertokens-web-js` SDK config at the root of your application: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... // highlight-start MultiFactorAuth.init(), Totp.init() // highlight-end ], }); ``` - Just like on the backend, we init the `totp` recipe in the `recipeList`. - We also init the `MultiFactorAuth` recipe, and pass in the first factors that we want to use. In this case, that would be `emailpassword` and `thirdparty` - same as the backend. Next, we need to add the TOTP pre-built UI when rendering the SuperTokens component: ```tsx function App() { return (
// highlight-start {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI])} // highlight-end // ... other routes
); } ```
```tsx function App() { // highlight-start if (canHandleRoute([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI])) { return getRoutingComponent([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI]) } // highlight-end return ( {/*Your app*/} ); } ```
:::success This step is not required for non React apps, since all the pre-built UI components are already added into the bundle. :::
With the above configuration, users will see `emailpassword` or social login UI when they visit the auth page. After completing that, users will be redirected to `/auth/mfa/totp` (assuming that the `websiteBasePath` is `/auth`) where they will be asked to setup the factor, or complete the TOTP challenge if they have already setup the factor before. The UI for this screen looks like: - [Factor Setup UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) - [Verification UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--verification-with-single-next-option) (In case the factor is already setup before).
We start by initialising the MFA and TOTP recipe on the frontend like so: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... // highlight-start MultiFactorAuth.init(), Totp.init() // highlight-end ], }); ``` ```tsx supertokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... // highlight-start supertokensMultiFactorAuth.init(), supertokensTotp.init() // highlight-end ], }); ``` :::success This step is not applicable for mobile apps. Please continue reading. ::: After the first factor login, you should start by [checking the access token payload and see if the MFA claim's `v` boolean is `false`](../frontend-setup#step-2-checking-the---custv-boolean-value-in-the-mfa-claim--cust). If it's not, then we can redirect the user to the application page. If it's `false`, the frontend then needs to [call the MFA endpoint](../frontend-setup#mfa-info-endpoint) to get information about which factor the user should be asked to complete next. Based on the backend config in this page, the `next` array will contain `["totp"]`. Two possibilities exist here: - Case 1: The user needs to setup a TOTP device cause they don't have any. - Case 2: The user already has a verified device setup and needs to complete the TOTP challenge. We can know which case it is by checking if `"totp"` is one of the items in the `factorsThatAreAlreadySetup` array that is returned from the API call above. If it is in the array, then it's case 2, otherwise it's case 1. #### Case 1 implementation: User needs to setup a new TOTP device In this case, we do two things: - Call an API on the backend to create a device. This returns the device secret that can be displayed to the user. The user is supposed to scan this using their authenticator app, to add a new entry for your app in their authenticator app. - Then the user needs to enter the TOTP code that's displayed to them in the app, and this needs to be sent to the backend to mark the device as verified. Once a device is marked as verified, only then will the `factorsThatAreAlreadySetup` array contain `"totp"` the next time they login. To create a new device, call the following API: ```tsx async function createNewTotpDevice() { if (await Session.doesSessionExist()) { try { let deviceResponse = await Totp.createDevice(); if (deviceResponse.status === "DEVICE_ALREADY_EXISTS_ERROR") { // this should only come here if you are passing a custom device name when calling the above function. throw new Error("Should never come here") // device created successfully } // device created successfully let qrCodeString = deviceResponse.qrCodeString; let secret = deviceResponse.secret; // TODO: display a QR code based on qrCodeString, and also an option to view // the secret if the user is unable to scan the QR code. } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("TOTP device creation can only happen after the first factor is complete and when a session exists") } } ``` ```tsx async function createNewTotpDevice() { if (await supertokensSession.doesSessionExist()) { try { let deviceResponse = await supertokensTotp.createDevice(); if (deviceResponse.status === "DEVICE_ALREADY_EXISTS_ERROR") { // this should only come here if you are passing a custom device name when calling the above function. throw new Error("Should never come here") // device created successfully } // device created successfully let qrCodeString = deviceResponse.qrCodeString; let secret = deviceResponse.secret; // TODO: display a QR code based on qrCodeString, and also an option to view // the secret if the user is unable to scan the QR code. } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("TOTP device creation can only happen after the first factor is complete and when a session exists") } } ``` The above API call returns the following response: ```json { "status": "OK", "issuerName": "...", "deviceName": "TOTP Device 1", "secret": "....", "userIdentifier": "user@example.com", "qrCodeString": "..." } | { "status": "DEVICE_ALREADY_EXISTS_ERROR" | "GENERAL_ERROR" } ``` - When device registration is successful, the API returns: - The `secret` and `qrCodeString` which are to be displayed to the user. For React apps, we recommend using the [react-qr-code library](https://github.com/rosskhanas/react-qr-code) to display the QR code. - The `issuerName` is the name will show up on the TOTP app for the user. By default, this is equal to the `appName` config on the backend SDK, however, you can change it to something else in the backend `totp.init` config. - The `userIdentifier` is the email / phone number of the user based on the first factor. This will also be shown in the TOTP app along with the `issuerName`. - The API call can also take a `deviceName` (as a POST body prop) which attempts to create a TOTP device with the provided name. A status of `"DEVICE_ALREADY_EXISTS_ERROR"` is returned in case a verified device with the input name already exists. In this case, you should ask the user to enter a different name. Note that this status is only returned in case you are passing in a custom device name. The default naming strategy is to name the device "TOTP Device N", where we start N from 1, and keep increasing it. This value can be used to identify a device from the backend point of view, for operations like deleting a device. - A status of `"GENERAL_ERROR"` is returned in case you specifically return that from a backend API override. Once a device has been created, and scanned, you need to ask the user to enter the TOTP and call the API below to verify it: ```tsx async function verifyTotpDevice(deviceName: string, userInputTotp: string) { if (await Session.doesSessionExist()) { try { let verifyResponse = await Totp.verifyDevice({ deviceName, totp: userInputTotp, }); if (verifyResponse.status === "UNKNOWN_DEVICE_ERROR") { // this can happen due to a race condition wherein the device is deleted before verifying. window.alert("Something went wrong. Please reload and try again"); } else if (verifyResponse.status === "LIMIT_REACHED_ERROR") { // this can happen if the user has entered a wrong TOTP too many times. window.alert("Totp incorrect. Please try again in " + (verifyResponse.retryAfterMs / 1000) + " seconds"); } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { window.alert("Totp incorrect. Please try again"); } else { // Device verified successfully } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("TOTP device verification can only happen after the first factor is complete and when a session exists") } } ``` ```tsx async function verifyTotpDevice(deviceName: string, userInputTotp: string) { if (await supertokensSession.doesSessionExist()) { try { let verifyResponse = await supertokensTotp.verifyDevice({ deviceName, totp: userInputTotp, }); if (verifyResponse.status === "UNKNOWN_DEVICE_ERROR") { // this can happen due to a race condition wherein the device is deleted before verifying. window.alert("Something went wrong. Please reload and try again"); } else if (verifyResponse.status === "LIMIT_REACHED_ERROR") { // this can happen if the user has entered a wrong TOTP too many times. window.alert("Totp incorrect. Please try again in " + (verifyResponse.retryAfterMs / 1000) + " seconds"); } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { window.alert("Totp incorrect. Please try again"); } else { // Device verified successfully } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("TOTP device verification can only happen after the first factor is complete and when a session exists") } } ``` The above API call returns the following response: ```json { "status": "OK", "wasAlreadyVerified": false } | { "status": "INVALID_TOTP_ERROR", "currentNumberOfFailedAttempts": 1, "maxNumberOfFailedAttempts": 5 } | { "status": "LIMIT_REACHED_ERROR", "retryAfterMs": 900000 } | { "status": "UNKNOWN_DEVICE_ERROR" | "GENERAL_ERROR" } ``` - The `deviceName`, which is an input to the API is one of the props returned from the previous API call to create a device. - When verification is successful (`status: "OK"`), the device is marked as verified in the database and can be used for the TOTP challenge next time around. The boolean `wasAlreadyVerified` indicates if the device was already verified before this call was made. - A status of `INVALID_TOTP_ERROR` means that the user has entered the an incorrect TOTP and needs to retry. The response contains two other props: - `currentNumberOfFailedAttempts`: The number of times the user has entered an incorrect TOTP so far. - `maxNumberOfFailedAttempts`: The maximum number of times the user can enter an incorrect TOTP before they are asked to wait (see `status: LIMIT_REACHED_ERROR`). This is set to 5 by default in the core. You can change this value by setting the `totp_max_attempts` in the core config. - A status of `LIMIT_REACHED_ERROR` indicates that the user has entered an incorrect TOTP too many times and must wait before trying again (otherwise even value TOTPs will fail). The waiting period is indicated by the `retryAfterMs` prop in the response body. By default, it is 15 minutes, but it can be changed by setting the value for `totp_rate_limit_cooldown_sec` in the core config. - A status of `UNKNOWN_DEVICE_ERROR` is possible due to a race condition in which the device is somehow deleted before the verification call is made. - A status of `GENERAL_ERROR` is possible if you specifically return that from a backend API override. On successful verification of a device, the `totp` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](../frontend-setup#mfa-info-endpoint). #### Case 2 implementation: User needs to complete the TOTP challenge This case is when the user already has a device setup (`totp` is in `factorsThatAreAlreadySetup`), and needs to complete the TOTP challenge. In this case, you should show the user an input box asking them to enter their TOTP from the authenticator app and then call the following API: ```tsx async function verifyTotpCode(userInputTotp: string) { if (await Session.doesSessionExist()) { try { let verifyResponse = await Totp.verifyCode({ totp: userInputTotp, }); if (verifyResponse.status === "LIMIT_REACHED_ERROR") { // this can happen if the user has entered a wrong TOTP too many times. window.alert("Totp incorrect. Please try again in " + (verifyResponse.retryAfterMs / 1000) + " seconds"); } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { window.alert("Totp incorrect. Please try again"); } else { // Code verified successfully } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("TOTP code verification can only happen after the first factor is complete and when a session exists") } } ``` ```tsx async function verifyTotpCode(userInputTotp: string) { if (await supertokensSession.doesSessionExist()) { try { let verifyResponse = await supertokensTotp.verifyCode({ totp: userInputTotp, }); if (verifyResponse.status === "LIMIT_REACHED_ERROR") { // this can happen if the user has entered a wrong TOTP too many times. window.alert("Totp incorrect. Please try again in " + (verifyResponse.retryAfterMs / 1000) + " seconds"); } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { window.alert("Totp incorrect. Please try again"); } else { // Code verified successfully } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("TOTP code verification can only happen after the first factor is complete and when a session exists") } } ``` The above API call returns the following response: ```json { "status": "OK" | "UNKNOWN_USER_ID_ERROR" } | { "status": "INVALID_TOTP_ERROR", "currentNumberOfFailedAttempts": 1, "maxNumberOfFailedAttempts": 5, } | { "status": "LIMIT_REACHED_ERROR", "retryAfterMs": 900000, } | { "status": "GENERAL_ERROR" } ``` - A `status: OK` indicates that verification was successful. SuperTokens tries and verifies the input TOTP against all verified devices that belong to this user. - A status of `INVALID_TOTP_ERROR` means that the user has entered the an incorrect TOTP and needs to retry. The response contains two other props: - `currentNumberOfFailedAttempts`: The number of times the user has entered an incorrect TOTP so far. - `maxNumberOfFailedAttempts`: The maximum number of times the user can enter an incorrect TOTP before they are asked to wait (see `status: LIMIT_REACHED_ERROR`). This is set to 5 by default in the core. You can change this value by setting the `totp_max_attempts` in the core config. - A status of `LIMIT_REACHED_ERROR` indicates that the user has entered an incorrect TOTP too many times and must wait before trying again (otherwise even value TOTPs will fail). The waiting period is indicated by the `retryAfterMs` prop in the response body. By default, it is 15 minutes, but it can be changed by setting the value for `totp_rate_limit_cooldown_sec` in the core config. - A status of `UNKNOWN_USER_ID_ERROR` is possible due to a race condition in which all devices that the user had are deleted by the time this API is called. In this case, you can ask users to setup a new device. - A status of `GENERAL_ERROR` is possible if you specifically return that from a backend API override. On successful verification of the code, the `totp` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](../frontend-setup#mfa-info-endpoint).
In a multi tenancy setup, you may want to enable TOTP for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the [single tenant setup](#single-tenant-setup) section above, so in this section, we will focus on enabling TOTP for all users within specific tenants. ### 1. Configure the backend To start, we will initialise the TOTP and the MultiFactorAuth recipes in the following way: ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), // highlight-start totp.init(), MultiFactorAuth.init() // highlight-end ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ totp.init(), multifactorauth.init(), ], ) ``` Unlike the single tenant setup, we do not provide any config to the `MultiFactorAuth` recipe cause all the necessary configuration will be done on a tenant level. To configure TOTP requirement for a tenant, we can call the following API: ```tsx async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], requiredSecondaryFactors: [MultiFactorAuth.FactorIds.TOTP] }); if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } } ``` - In the above, we set the `firstFactors` to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. - We set the `requiredSecondaryFactors` to `["totp"]` to indicate that TOTP is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds async def create_new_tenant(): resp = await create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD], required_secondary_factors=[FactorIds.TOTP], ) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds def create_new_tenant(): resp = create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD], required_secondary_factors=[FactorIds.TOTP], ) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished `totp`, the payload will look like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "totp": 1702877999 }, "v": true } } ``` Indicating that the user has finished all required factors, and should be allowed to access the app. ### 2. Configure the frontend We start by modifying the `init` function call on the frontend like so: ```tsx supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, // highlight-next-line usesDynamicLoginMethods: true, recipeList: [ // other recipes... // highlight-start totp.init(), MultiFactorAuth.init(), Multitenancy.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getTenantId: async (context) => { return "TODO" } } } } }) // highlight-end ] }) ``` You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: This change is in your auth route config. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, // highlight-next-line usesDynamicLoginMethods: true, recipeList: [ // other recipes... // highlight-start supertokensUITOTP.init(), supertokensUIMultiFactorAuth.init(), supertokensUIMultitenancy.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getTenantId: async (context) => { return "TODO" } } } } }) // highlight-end ] }) ``` This change goes in the `supertokens-web-js` SDK config at the root of your application: ```tsx supertokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // highlight-start Session.init(), MultiFactorAuth.init(), // highlight-end ], }); ``` - Just like on the backend, we init the `totp` recipe in the `recipeList`. - We also init the `MultiFactorAuth` recipe. Notice that unlike the single tenant setup, we do not specify the `firstFactors` here. That information is fetched based on the tenantId you provide the SDK with. - We have set `usesDynamicLoginMethods: true` so that the SDK knows to fetch the login methods dynamically based on the tenantId. - Finally, we init the multi tenancy recipe and provide a method for getting the tenantId. Next, we need to add the TOTP pre-built UI when rendering the SuperTokens component: ```tsx function App() { return (
// highlight-start {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI])} // highlight-end // ... other routes
); } ```
```tsx function App() { // highlight-start if (canHandleRoute([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI])) { return getRoutingComponent([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI]) } // highlight-end return ( {/*Your app*/} ); } ```
:::success This step is not required for non React apps, since all the pre-built UI components are already added into the bundle. :::
With the above configuration, users will see the first and second factor based on the tenant configuration. For the tenant we configured above, users will see email password or social login first. After completing that, users will be redirected to `/auth/mfa/totp` (assuming that the `websiteBasePath` is `/auth`) where they will be asked to setup the factor, or complete the TOTP challenge if they have already setup the factor before. The UI for this screen looks like: - [Factor Setup UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) - [Verification UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--verification-with-single-next-option) (In case the factor is already setup before).
The steps here are the same as in [the single tenant setup above](#frontend-setup).
# Additional Verification - Multi Factor Authentication - TOTP - Require TOTP for specific users Source: https://supertokens.com/docs/additional-verification/mfa/totp/totp-for-opt-in-users ## Overview In this page, we will show you how to implement an MFA policy that requires certain users to do TOTP. You can decide which those users are based on any criteria. For example: - Only users that have an `admin` role require to do TOTP; OR - Only users that have enabled TOTP on their account require to do TOTP; OR - Only users that have a paid account require to do TOTP. Whatever the criteria is, the steps to implementing this type of a flow is the same. ## Before you start The tutorial assumes that the first factor is email password or social login, but the same set of steps are applicable for other first factor types. ## Steps ### 1. Configure the backend #### Enable TOTP for users that have an `admin` role To start with, we configure the backend in the following way: ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ Session.init(), UserRoles.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), // highlight-next-line totp.init(), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id) if (roles.roles.includes("admin")) { // we only want totp for admins return [MultiFactorAuth.FactorIds.TOTP] } else { // no MFA for non admin users. return [] } } } } } // highlight-end }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, MFARequirementList, ) from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.recipe.userroles.asyncio import get_roles_for_user def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # Get roles for the user roles = await get_roles_for_user(tenant_id, (await user()).id) if "admin" in roles.roles: # We only want OTP_EMAIL for admins return [FactorIds.TOTP] else: # No MFA for non-admin users return [] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ totp.init(), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` We override the `getMFARequirementsForAuth` function to indicate that `totp` must be completed only for users that have the `admin` role. You can also have any other criteria here. #### Ask for TOTP only for users that have enabled TOTP on their account To start with, we configure the backend in the following way: ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), // highlight-start totp.init({ override: { apis: (oI) => { return { ...oI, verifyDevicePOST: async function (input) { let response = await oI.verifyDevicePOST!(input); if (response.status === "OK") { // device successfully verified. We save that this user has enabled TOTP in the user metadata. // The multifactorauth recipe will pick this value up next time the user is trying to login, and // ask them to enter the TOTP code. await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(input.session.getUserId(), MultiFactorAuth.FactorIds.TOTP); } return response; } } } } }), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], }) // highlight-end ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, ) from typing import Dict, Any from supertokens_python.recipe.totp.types import ( TOTPConfig, OverrideConfig, VerifyDeviceOkResult, ) from supertokens_python.recipe.totp.interfaces import APIInterface, APIOptions from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, ) def totp_override(original_implementation: APIInterface): original_verify_device_post = original_implementation.verify_device_post async def verify_device_post( device_name: str, totp: str, options: APIOptions, session: SessionContainer, user_context: Dict[str, Any], ): response = await original_verify_device_post( device_name, totp, options, session, user_context ) if isinstance(response, VerifyDeviceOkResult): await add_to_required_secondary_factors_for_user( session.get_user_id(), FactorIds.TOTP ) return response original_implementation.verify_device_post = verify_device_post return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ totp.init(TOTPConfig(override=OverrideConfig(apis=totp_override))), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY] ), ], ) ``` We initialise the multi factor auth recipe here without any override to `getMFARequirementsForAuth`. The default implementation of this function already checks what factors are enabled for a user and returns those. Therefore all we need to do is mark `totp` as enabled for a user as soon as they have setup a device successfully. This happens in the `verifyDevicePOST` API override as shown above. Once a device is verified, we mark the `totp` factor as enabled for the user, and the next time they login, they will be asked to complete the TOTP challenge. In both of the examples above, notice that we have initialised the TOTP recipe in the `recipeList`. Here are some of the configrations you can add to the `totp.init` function: - `issuer`: This is the name that will show up in the TOTP app for the user. By default, this is equal to the `appName` config, however, you can change it to something else using this property. - `defaultSkew`: The default value of this is `1`, which means that TOTP codes that were generated 1 tick before, and that will be generated 1 tick after from the current tick will be accepted at any given time (including the TOTP of the current tick, of course). - `defaultPeriod`: The default value of this is `30`, which means that the current tick is value for 30 seconds. By default, a TOTP code that's shown to the user, is valid for 60 seconds (`defaultPeriod + defaultSkew*defaultPeriod` seconds) Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this (for those that require TOTP): ```json { "st-mfa": { "c": { "emailpassword": 1702877939, }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished TOTP, the payload will look like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "totp": 1702877999 }, "v": true } } ``` Indicating that the user has finished all required factors, and should be allowed to access the app. ### 2. Configure the frontend Two parts exist to this: - Configuring the frontend to show the TOTP UI when required during login / sign up - Allowing users to enable / disable TOTP on their account via the settings page (If you are following Example 2 from above). The first part is identical to the steps mentioned in [this section](./totp-for-all-users#frontend-setup), please follow that. The second part, which is only applicable in case you want to allow users to enable / disable TOTP themselves, can be achieved by creating the following flow on your frontend: - When the user navigates to their settings page, you can show them if TOTP is enabled or not. - If enabled, you can show them a list of current TOTP devices with options to remove any. - If enabled, you can show them an option to add a new TOTP device. In order to know if the user has enabled TOTP, you can make an API your backend which calls the following function: ```ts async function isTotpEnabledForUser(userId: string) { let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId) return factors.includes(MultiFactorAuth.FactorIds.TOTP) } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds async def is_totp_factor_enabled_for_user(user_id: str) -> bool: factors = await get_required_secondary_factors_for_user(user_id, {}) return FactorIds.TOTP in factors ``` ```python from supertokens_python.recipe.multifactorauth.syncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds def is_totp_factor_enabled_for_user(user_id: str) -> bool: factors = get_required_secondary_factors_for_user(user_id, {}) return FactorIds.TOTP in factors ``` If the user wants to enable or disable TOTP for them, you can make an API on your backend which calls the following function: ```ts async function enableMFAForUser(userId: string) { await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP) } async function disableMFAForUser(userId: string) { await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP) } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, remove_from_required_secondary_factors_for_user, ) from supertokens_python.recipe.multifactorauth.types import FactorIds async def enable_mfa_for_user(user_id: str) -> None: await add_to_required_secondary_factors_for_user(user_id, FactorIds.TOTP) async def disable_mfa_for_user(user_id: str) -> None: await remove_from_required_secondary_factors_for_user(user_id, FactorIds.TOTP) ``` ```python from supertokens_python.recipe.multifactorauth.syncio import ( add_to_required_secondary_factors_for_user, remove_from_required_secondary_factors_for_user, ) from supertokens_python.recipe.multifactorauth.types import FactorIds def enable_mfa_for_user(user_id: str) -> None: add_to_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL) def disable_mfa_for_user(user_id: str) -> None: remove_from_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL) ``` In order to list existing TOTP devices on the frontend, you can call the following API: ```tsx async function fetchTOTPDevices() { if (await Session.doesSessionExist()) { try { let totpDevicesResponse = await Totp.listDevices(); for (let i = 0; i < totpDevicesResponse.devices.length; i++) { let currDevice = totpDevicesResponse.devices[i]; console.log(currDevice.name) // by default, this will be like "TOTP Device 1" console.log(currDevice.verified) } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists") } } ``` ```tsx async function fetchTOTPDevices() { if (await supertokensSession.doesSessionExist()) { try { let totpDevicesResponse = await supertokensTotp.listDevices(); for (let i = 0; i < totpDevicesResponse.devices.length; i++) { let currDevice = totpDevicesResponse.devices[i]; console.log(currDevice.name) // by default, this will be like "TOTP Device 1" console.log(currDevice.verified) } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists") } } ``` Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): ```bash curl --location --request GET '/totp/device/list' \ --header 'Authorization: Bearer ...' ``` The output from the API call is as follows: ```json { "status": "OK", "devices": { "name": "TOTP Device 1", "period": 30, "skew": 1, "verified": true }[]; } | { "status": "GENERAL_ERROR" } ``` - A `status: OK` will contain the list of all devices that exist for this user, across all of the user's tenants. We recommend only showing the devices that are `verified` to the user. - A `status: GENERAL_ERROR`: This is possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend In order to remove a device, you can call the following API from the frontend: ```tsx async function removeTOTPDevices(deviceName: string) { if (await Session.doesSessionExist()) { try { await Totp.removeDevice({ deviceName }); // device is removed } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists") } } ``` ```tsx async function removeTOTPDevices(deviceName: string) { if (await supertokensSession.doesSessionExist()) { try { await supertokensTotp.removeDevice({ deviceName }); // device is removed } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists") } } ``` Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): ```bash curl --location --request POST '/totp/device/remove' \ --header 'Authorization: Bearer ...' --header 'Content-Type: application/json' \ --data-raw '{ "deviceName": "..." }' ``` The output from the API call is as follows: ```json { "status": "OK", "didDeviceExist": true; } | { "status": "GENERAL_ERROR" } ``` In order to add a new device, you can call the following function from the frontend. This function will redirect the user to the TOTP create device pre-built UI. After the user has finished the new device creation and verification, they will be redirected back to the current page: ```tsx async function redirectToTotpSetupScreen() { MultiFactorAuth.redirectToFactor({ factorId: "totp", forceSetup: true, redirectBack: true, }) } ``` - In the snippet above, we redirect to the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option). We set the `forceSetup` to `true` since we want the user to setup a new TOTP device. The `redirectBack` boolean is also `true` since we want to redirect back to the current page after the user has finished setting up the device. - You can also redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` if you don't want to use the above function. In order to add a new device, you can redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` from your settings page. This will show the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) to the user: - We add the query param of `setup=true` because we want to create a new device. - The `redirectToPath` query param will also tell our SDK to redirect the user back to the current page after they have finished creating the device. After the user has finished creating a device, our backend override for `verifyDevicePOST` (see "Example 2" in [Backend setup section](#backend-setup) above) will add TOTP as a required factor for this user, ensuring that next time they login, they will be asked to complete the TOTP challenge. In order to create a new device, you should redirect the user to a page which creates a new TOTP device on the backend, asks the user to scan the QR code and then to enter the TOTP in order to verify the new device. This can be achieved by calling the functions mentioned in [this section](./totp-for-all-users#case-1-implementation-user-needs-to-setup-a-new-totp-device--cust). ### 1. Configure the backend A user can be a part of multiple tenants. If you want TOTP to be enabled for a specific user across all the tenants that they are a part of, the steps are the same as in the [Backend setup](#backend-setup) section above. However, if you want TOTP to be enabled for a specific user, for a specific tenant (or a sub set of tenants that the user is a part of), then you will have to add additional logic to the `getMFARequirementsForAuth` function override. Modifying the example code from the [Backend setup](#backend-setup) section above: #### Only enable TOTP for users that have an `admin` role ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ Session.init(), UserRoles.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), // highlight-next-line totp.init(), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id) // highlight-next-line if (roles.roles.includes("admin") && (await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.TOTP)) { // we only want totp for admins return [MultiFactorAuth.FactorIds.TOTP] } else { // no MFA for non admin users. return [] } } } } } }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, MFARequirementList, ) from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.recipe.userroles.asyncio import get_roles_for_user def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # Get roles for the user roles = await get_roles_for_user(tenant_id, (await user()).id) if ( "admin" in roles.roles and FactorIds.TOTP in await required_secondary_factors_for_tenant() ): # We only want OTP_EMAIL for admins return [FactorIds.TOTP] else: # No MFA for non-admin users return [] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), totp.init(), ], ) ``` - The implementation of `shouldRequireTotpForTenant` is entirely up to you. #### Ask for TOTP only for users that have enabled TOTP on their account ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), totp.init({ override: { apis: (oI) => { return { ...oI, verifyDevicePOST: async function (input) { let response = await oI.verifyDevicePOST!(input); if (response.status === "OK") { // device successfully verified. We save that this user has enabled TOTP in the user metadata. // The multifactorauth recipe will pick this value up next time the user is trying to login, and // ask them to enter the TOTP code. await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(input.session.getUserId(), MultiFactorAuth.FactorIds.TOTP); } return response; } } } } }), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { if ((await input.requiredSecondaryFactorsForUser).includes(MultiFactorAuth.FactorIds.TOTP)) { // this means that the user has finished setting up a device from their settings page. if ((await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.TOTP)) { return [MultiFactorAuth.FactorIds.TOTP] } } // no totp required for input.user, with the input.tenant. return [] } } } } // highlight-end }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig as MFAOverrideConfig, MFARequirementList, ) from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, ) from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.recipe.totp.interfaces import APIInterface, APIOptions from supertokens_python.recipe.totp.types import ( TOTPConfig, OverrideConfig, VerifyDeviceOkResult, ) def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: if FactorIds.TOTP in await required_secondary_factors_for_user(): if FactorIds.TOTP in await required_secondary_factors_for_tenant(): return [FactorIds.TOTP] # no otp-email required for input.user, with the input.tenant. return [] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation def totp_override(original_implementation: APIInterface): original_verify_device_post = original_implementation.verify_device_post async def verify_device_post( device_name: str, totp: str, options: APIOptions, session: SessionContainer, user_context: Dict[str, Any], ): response = await original_verify_device_post( device_name, totp, options, session, user_context ) if isinstance(response, VerifyDeviceOkResult): await add_to_required_secondary_factors_for_user( session.get_user_id(), FactorIds.TOTP ) return response original_implementation.verify_device_post = verify_device_post return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ totp.init(TOTPConfig(override=OverrideConfig(apis=totp_override))), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=MFAOverrideConfig(functions=override_functions), ), ], ) ``` - We provide an override for `getMFARequirementsForAuth` which checks if TOTP is enabled for the user, and also take into account the tenantId to decide if we want to have this user go through the TOTP flow whilst logging into this tenant. The implementation of `shouldRequireTotpForTenant` is entirely up to you. ### 2. Configure the frontend Two parts exist to this: - Configuring the frontend to show the TOTP UI when required during login / sign up - Allowing users to enable / disable TOTP on their account via the settings page (If you are following Example 2 from above). The first part is identical to the steps mentioned in [this section](/docs/additional-verification/mfa/totp/totp-for-all-users#2-configure-the-), so please follow that. The second part, which is only applicable in case you want to allow users to enable / disable TOTP themselves, can be achieved by creating the following flow on your frontend: - When the user navigates to their settings page, you can show them if TOTP is enabled or not. - If enabled, you can show them a list of current TOTP devices with options to remove any. - If enabled, you can show them an option to add a new TOTP device. In order to know if the user has enabled TOTP, you can make an API your backend which calls the following function: ```ts async function isTotpEnabledForUser(userId: string) { let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId) return factors.includes(MultiFactorAuth.FactorIds.TOTP) } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds async def is_totp_factor_enabled_for_user(user_id: str) -> bool: factors = await get_required_secondary_factors_for_user(user_id, {}) return FactorIds.TOTP in factors ``` ```python from supertokens_python.recipe.multifactorauth.syncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds def is_totp_factor_enabled_for_user(user_id: str) -> bool: factors = get_required_secondary_factors_for_user(user_id, {}) return FactorIds.TOTP in factors ``` If the user wants to enable or disable TOTP for them, you can make an API on your backend which calls the following function: ```ts async function enableMFAForUser(userId: string) { await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP) } async function disableMFAForUser(userId: string) { await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP) } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, remove_from_required_secondary_factors_for_user, ) from supertokens_python.recipe.multifactorauth.types import FactorIds async def enable_mfa_for_user(user_id: str) -> None: await add_to_required_secondary_factors_for_user(user_id, FactorIds.TOTP) async def disable_mfa_for_user(user_id: str) -> None: await remove_from_required_secondary_factors_for_user(user_id, FactorIds.TOTP) ``` ```python from supertokens_python.recipe.multifactorauth.syncio import ( add_to_required_secondary_factors_for_user, remove_from_required_secondary_factors_for_user, ) from supertokens_python.recipe.multifactorauth.types import FactorIds def enable_mfa_for_user(user_id: str) -> None: add_to_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL) def disable_mfa_for_user(user_id: str) -> None: remove_from_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL) ``` In order to list existing TOTP devices on the frontend, you can call the following API: ```tsx async function fetchTOTPDevices() { if (await Session.doesSessionExist()) { try { let totpDevicesResponse = await Totp.listDevices(); for (let i = 0; i < totpDevicesResponse.devices.length; i++) { let currDevice = totpDevicesResponse.devices[i]; console.log(currDevice.name) // by default, this will be like "TOTP Device 1" console.log(currDevice.verified) } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists") } } ``` ```tsx async function fetchTOTPDevices() { if (await supertokensSession.doesSessionExist()) { try { let totpDevicesResponse = await supertokensTotp.listDevices(); for (let i = 0; i < totpDevicesResponse.devices.length; i++) { let currDevice = totpDevicesResponse.devices[i]; console.log(currDevice.name) // by default, this will be like "TOTP Device 1" console.log(currDevice.verified) } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists") } } ``` Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): ```bash curl --location --request GET '/totp/device/list' \ --header 'Authorization: Bearer ...' ``` The output from the API call is as follows: ```json { "status": "OK", "devices": { "name": "TOTP Device 1", "period": 30, "skew": 1, "verified": true }[]; } | { "status": "GENERAL_ERROR" } ``` - A `status: OK` will contain the list of all devices that exist for this user, across all of the user's tenants. We recommend only showing the devices that are `verified` to the user. - A `status: GENERAL_ERROR`: This is possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend In order to remove a device, you can call the following API from the frontend: ```tsx async function removeTOTPDevices(deviceName: string) { if (await Session.doesSessionExist()) { try { await Totp.removeDevice({ deviceName }); // device is removed } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists") } } ``` ```tsx async function removeTOTPDevices(deviceName: string) { if (await supertokensSession.doesSessionExist()) { try { await supertokensTotp.removeDevice({ deviceName }); // device is removed } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists") } } ``` Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): ```bash curl --location --request POST '/totp/device/remove' \ --header 'Authorization: Bearer ...' --header 'Content-Type: application/json' \ --data-raw '{ "deviceName": "..." }' ``` The output from the API call is as follows: ```json { "status": "OK", "didDeviceExist": true; } | { "status": "GENERAL_ERROR" } ``` In order to add a new device, you can call the following function from the frontend. This function will redirect the user to the TOTP create device pre-built UI. After the user has finished the new device creation and verification, they will be redirected back to the current page: ```tsx async function redirectToTotpSetupScreen() { MultiFactorAuth.redirectToFactor({ factorId: "totp", forceSetup: true, redirectBack: true, }) } ``` - In the snippet above, we redirect to the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option). We set the `forceSetup` to `true` since we want the user to setup a new TOTP device. The `redirectBack` boolean is also `true` since we want to redirect back to the current page after the user has finished setting up the device. - You can also redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` if you don't want to use the above function. In order to add a new device, you can redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` from your settings page. This will show the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) to the user: - We add the query param of `setup=true` because we want to create a new device. - The `redirectToPath` query param will also tell our SDK to redirect the user back to the current page after they have finished creating the device. After the user has finished creating a device, our backend override for `verifyDevicePOST` (see "Example 2" in [Backend setup section](#1-configure-the-backend) above) will add TOTP as a required factor for this user, so that next time they login, they will be asked to complete the TOTP challenge. In order to create a new device, you should redirect the user to a page which creates a new TOTP device on the backend, asks the user to scan the QR code and then to enter the TOTP in order to verify the new device. This can be achieved by calling the functions mentioned in [this section](./totp-for-all-users#case-1-implementation-user-needs-to-setup-a-new-totp-device--cust). # Additional Verification - Multi Factor Authentication - OTP - OTP required for all users Source: https://supertokens.com/docs/additional-verification/mfa/email-sms-otp/otp-for-all-users This page shows how to implement an MFA policy that requires all users to complete an OTP challenge before accessing your application. The OTP can be sent via email or phone. :::note Assume that the first factor is email password or social login, but the same set of steps applies to other first factor types as well. ::: ## Single tenant setup ### Backend setup To start with, configure the backend in the following way: ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), // highlight-start Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE" }), AccountLinking.init({ shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { if (session === undefined) { // we do not want to do first factor account linking by default. To enable that, // please see the automatic account linking docs in the recipe docs for your first factor. return { shouldAutomaticallyLink: false }; } if (user === undefined || session.getUserId() === user.id) { // if it comes here, it means that a session exists, and we are trying to link the // newAccountInfo to the session user, which means it's an MFA flow, so we enable // linking here. return { shouldAutomaticallyLink: true, shouldRequireVerification: false } } return { shouldAutomaticallyLink: false }; } }), // highlight-end MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { return [MultiFactorAuth.FactorIds.OTP_EMAIL] } } } } // highlight-end }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, accountlinking from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig, MFARequirementList from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink, ) from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List, Optional, Union async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any] ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is None: # We do not want to do first factor account linking by default. # To enable that, please see the automatic account linking docs # in the recipe docs for your first factor. return ShouldNotAutomaticallyLink() if user is None or session.get_user_id() == user.id: # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. return ShouldAutomaticallyLink(should_require_verification=False) return ShouldNotAutomaticallyLink() def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # highlight-next-line return [FactorIds.OTP_EMAIL] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), accountlinking.init(should_do_automatic_account_linking=should_do_automatic_account_linking) ], ) ``` - Notice that the Passwordless recipe initializes in the `recipeList`. In this example, only email-based OTP is enabled, with `contactMethod` set to `EMAIL` and `flowType` to `USER_INPUT_CODE` (that is, `otp`). If you want to use phone SMS-based OTP, set the contact method to `PHONE`. If you want to give users both options, or for some users use email, and for others use phone, set `contactMethod` to `EMAIL_OR_PHONE`. - We have also enabled the account linking feature since it's required for MFA to work. The above enables account linking for second factor only, but if you also want to enable it for first factor, see [this section](/docs/post-authentication/account-linking/automatic-account-linking). - Notice that `shouldRequireVerification: false` configures account linking. It means that the second factor can link to the first factor even though the first factor is not verified. If you want to do email verification of the first factor before setting up the second factor (for example if the first factor is email password, and the second is phone OTP), then you can set this boolean to `true`, and also init the email verification recipe on the frontend and backend in `REQUIRED` mode. - The `getMFARequirementsForAuth` function is overridden to indicate that `otp-email` must be completed before the user can access the app. Notice that `userId` is not checked there, and `otp-email` is returned for all users. You can also return `otp-phone` instead if you want users to complete the OTP challenge via a phone SMS. Finally, if you want to give users an option for email or phone, you can return the following array from the function: ```json [{ "oneOf": ["otp-email", "otp-phone"] }] ``` Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload looks like this: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished `otp-email`, the payload looks like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "otp-email": 1702877999 }, "v": true } } ``` Indicating that the user has finished all required factors, and should be allowed to access the app. :::caution If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` and `flowType` are set correctly. ::: ### Frontend setup We start by modifying the `init` function call on the frontend like this: ```tsx supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init(/* ... */), EmailPassword.init( /* ... */), // highlight-start Passwordless.init({ contactMethod: "EMAIL" }), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ] }) // highlight-end ] }) ``` You have to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ supertokensUIThirdParty.init(/* ... */), supertokensUIEmailPassword.init( /* ... */), // highlight-start supertokensUIPasswordless.init({ contactMethod: "EMAIL" }), supertokensUIMultiFactorAuth.init({ firstFactors: [ supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY ] }) // highlight-end ] }) ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... // highlight-start MultiFactorAuth.init(), Passwordless.init() // highlight-end ], }); ``` - Like on the backend, the `passwordless` recipe initializes in the `recipeList`. The `contactMethod` needs to be consistent with the backend setting. - The `MultiFactorAuth` recipe is also initialized, and the first factors to use are included. In this case, that would be `emailpassword` and `thirdparty` - same as the backend. Next, add the Passwordless pre-built UI when rendering the SuperTokens component: ```tsx function App() { return (
// highlight-start {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI])} // highlight-end // ... other routes
); } ```
```tsx function App() { // highlight-start if (canHandleRoute([EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI])) { return getRoutingComponent([EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI]) } // highlight-end return ( {/*Your app*/} ); } ```
:::success This step is not required for non React apps, since all the pre-built UI components are already added into the bundle. :::
With the above configuration, users see `emailpassword` or social login UI when they visit the auth page. After completing that, users redirect to `/auth/mfa/otp-email` (assuming that the `websiteBasePath` is `/auth`) where they are asked to complete the OTP challenge. The UI for this screen looks like: - [Factor Setup UI](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/passwordless-mfa--setup-email) (This is in case the first factor doesn't provide an email for the user. In this example, the first factor does provide an email since it's email password or social login). - [Verification UI](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/passwordless-mfa--verification). :::caution If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` is set correctly. :::
We start by initializing the MFA and Passwordless recipe on the frontend like this: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... // highlight-start MultiFactorAuth.init(), Passwordless.init() // highlight-end ], }); ``` ```tsx supertokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... // highlight-start supertokensMultiFactorAuth.init(), supertokensPasswordless.init() // highlight-end ], }); ``` :::success This step is not applicable for mobile apps. Please continue reading. ::: After the first factor login, you should start by [checking the access token payload and see if the MFA claim's `v` boolean is `false`](../frontend-setup#step-2-checking-the---custv-boolean-value-in-the-mfa-claim--cust). If it's not, then the user can redirect to the application page. If it's `false`, the frontend then needs to [call the MFA endpoint](../frontend-setup#mfa-info-endpoint) to get information about which factor the user should complete next. Based on the backend configuration in this page, the `next` array contains `["otp-email"]`. Two possibilities exist here: - Case 1: The user needs to set up an email to send the OTP to. This only happens if the first factor doesn't provide an email from the user (for example, if you used phone-based `otp` as the first factor). In this example on this doc, an email is always obtained from the first factor, so you do not need to build UI for this step (but this will still be discussed later on). - Case 2: The user already has an email associated with them and need to complete the OTP challenge. We can know which case it is by checking if the `emails` object returned from [MFA Info endpoint](../frontend-setup#mfa-info-endpoint) contains any emails associated with the `otp-email` key. If the `emails["otp-email"]` property of the response is `undefined` or an empty array, then it's case 1, else it's case 2. #### Case 1 implementation: User needs to enter their email In this case, a form needs to be created wherein the user can enter their email. Once they submit the form, the [`createCode` API](/docs/passwordless/custom-ui/login-otp) needs to be called. After this API call, you can show the user the enter OTP screen, and call the [`consumeCode` API](/docs/passwordless/custom-ui/login-otp#step-3-verifying-the-input-otp). If the API call returns a `RESTART_FLOW_ERROR`, you can handle this by asking the user to enter their email once again and then call the `createCode` function. #### Case 2 implementation: User needs to complete the OTP challenge This case is when the user already has an email associated with their account and you can directly send a code to that email. You can get the email to send the code to from the result of the [MFA Info endpoint](../frontend-setup#mfa-info-endpoint). Specifically, from the response, you can read the email from the `emails` property like this: `emails["otp-email"][0]`. The first item in the array of email is picked since the emails are ordered based on: - Index 0 contains the email that belongs to the session's user. If the user's first factor was email password, the email in the 0th index of the array is that email. - The other emails in the array (if they exist), are from other login methods for this user ordered based on the oldest login method first. You can even show a UI here asking the user to pick an email from the array if you like. Either way, when you have an email, you can call the [`createCode` API](/docs/passwordless/custom-ui/login-otp) to send the code to that email. After this API call, you can show the user the enter OTP screen, and call the [`consumeCode` API](/docs/passwordless/custom-ui/login-otp#step-3-verifying-the-input-otp). If the API call returns a `RESTART_FLOW_ERROR`, you can handle this by calling the `createCode` function once again in the background. :::note Notice that in Case 2, there is no UI for the user to enter an email. That happens. The user only sees the enter OTP screen. ::: We recommend that you add a sign out button when showing the second factor (case 1 or case 2) so that users can use this to escape out of the flow in case they are unable to complete the second factor. When the sign out button is clicked, you want to: - Call the `await clearLoginAttemptInfo()` function (if on web) to clear the state that's set in the browser storage when calling the `createCode` function. - Call the sign out function / API to clear the tokens. On successful verification of the code, the `otp-email` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](../frontend-setup#mfa-info-endpoint). ## Multi tenant setup In a multi-tenancy setup, you may want to enable email / phone OTP for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the [single tenant setup](#single-tenant-setup) section above, so in this section, we will focus on enabling OTP for all users within specific tenants. ### Backend setup To start, initialize the Passwordless and the MultiFactorAuth recipes in the following way: ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), // highlight-start Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE" }), AccountLinking.init({ shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { if (session === undefined) { // we do not want to do first factor account linking by default. To enable that, // please see the automatic account linking docs in the recipe docs for your first factor. return { shouldAutomaticallyLink: false }; } if (user === undefined || session.getUserId() === user.id) { // if it comes here, it means that a session exists, and we are trying to link the // newAccountInfo to the session user, which means it's an MFA flow, so we enable // linking here. return { shouldAutomaticallyLink: true, shouldRequireVerification: false } } return { shouldAutomaticallyLink: false }; } }), MultiFactorAuth.init() // highlight-end ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, accountlinking from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink, ) from supertokens_python.types import User from typing import Dict, Any, Optional, Union async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any] ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is None: # We do not want to do first factor account linking by default. # To enable that, please see the automatic account linking docs # in the recipe docs for your first factor. return ShouldNotAutomaticallyLink() if user is None or session.get_user_id() == user.id: # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. return ShouldAutomaticallyLink(should_require_verification=False) return ShouldNotAutomaticallyLink() init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ multifactorauth.init(), accountlinking.init(should_do_automatic_account_linking=should_do_automatic_account_linking) ], ) ``` Unlike the single tenant setup, no configuration is provided to the `MultiFactorAuth` recipe because all the necessary configuration is done on a tenant level. To configure otp-email requirement for a tenant, the following API can be called: ```tsx async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], requiredSecondaryFactors: [MultiFactorAuth.FactorIds.OTP_EMAIL] }); if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } } ``` - In the above, the `firstFactors` are set to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. - The `requiredSecondaryFactors` is set to `["otp-email"]` to indicate that OTP email is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds async def create_new_tenant(): resp = await create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], required_secondary_factors=[FactorIds.OTP_EMAIL], ), ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds def create_new_tenant(): resp = create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], required_secondary_factors=[FactorIds.OTP_EMAIL], ), ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` To configure otp-email requirement for a tenant, the following API can be called: ```bash curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data-raw '{ "tenantId": "customer1", "firstFactors": ["emailpassword", "thirdparty"], "requiredSecondaryFactors": ["otp-email"] }' ``` - In the above, the `firstFactors` are set to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. - The `requiredSecondaryFactors` is set to `["otp-email"]` to indicate that OTP email is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. Enable EmailPassword and ThirdParty, OTP-Email As shown above, enable **Email Password** and **Third Party** in the Login methods section and enable **OTP - Email** in the Secondary Factors Section. Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload looks like this: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished otp-email challenge, the payload looks like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "otp-email": 1702877999 }, "v": true } } ``` Indicating that the user has finished all required factors, and should be allowed to access the app. :::caution If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` and `flowType` are set correctly. ::: ### Frontend setup We start by modifying the `init` function call on the frontend like this: ```tsx supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, // highlight-next-line usesDynamicLoginMethods: true, recipeList: [ ThirdParty.init({ //... }), EmailPassword.init({ //... }), // highlight-start Passwordless.init({ contactMethod: "EMAIL" }), MultiFactorAuth.init(), Multitenancy.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getTenantId: async (context) => { return "TODO" } } } } }) // highlight-end ] }) ``` You have to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, // highlight-next-line usesDynamicLoginMethods: true, recipeList: [ supertokensUIThirdParty.init({ //... }), supertokensUIEmailPassword.init({ //... }), // highlight-start supertokensUIPasswordless.init({ contactMethod: "EMAIL" }), supertokensUIMultiFactorAuth.init(), supertokensUIMultitenancy.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getTenantId: async (context) => { return "TODO" } } } } }) // highlight-end ] }) ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx supertokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // highlight-start Session.init(), MultiFactorAuth.init(), // highlight-end ], }); ``` - Like on the backend, the `Passwordless` recipe initializes in the `recipeList`. Make sure that the configuration for it is consistent with what's on the backend. - The `MultiFactorAuth` recipe is also initialized. Notice that unlike the single tenant setup, the `firstFactors` are not specified here. That information is fetched based on the `tenantId` you provide the SDK with. - `usesDynamicLoginMethods: true` is set so that the SDK knows to fetch the login methods dynamically based on the `tenantId`. - Finally, the multi-tenancy recipe initializes and a method for getting the `tenantId` is provided. Next, add the Passwordless pre-built UI when rendering the SuperTokens component: ```tsx function App() { return (
// highlight-start {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI])} // highlight-end // ... other routes
); } ```
```tsx function App() { // highlight-start if (canHandleRoute([EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI])) { return getRoutingComponent([EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI]) } // highlight-end return ( {/*Your app*/} ); } ```
:::success This step is not required for non React apps, since all the pre-built UI components are already added into the bundle. :::
With the above configuration, users see the first and second factor based on the tenant configuration. For the tenant configured above, users see email password or social login first. After completing that, users redirect to `/auth/mfa/otp-email` (assuming that the `websiteBasePath` is `/auth`) where they are asked to complete the OTP challenge. The UI for this screen looks like: - [Factor Setup UI](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/passwordless-mfa--setup-email) (This is in case the first factor doesn't provide an email for the user. In this example, the first factor does provide an email since it's email password or social login). - [Verification UI](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/passwordless-mfa--verification). :::caution If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` is set correctly. :::
The steps here are the same as in [the single tenant setup above](#frontend-setup). ## Protecting frontend and backend routes See the section on [protecting frontend and backend routes](../protect-routes). ## Email / SMS sending and design By default, the email template used for otp-email login is [as shown here](https://github.com/SuperTokens/email-sms-templates?tab=readme-ov-file#otp-login), and the default SMS template is [as shown here](https://github.com/SuperTokens/email-sms-templates?tab=readme-ov-file#otp-login-1). The method for sending them is via an email and SMS sending service that is provided. If you would like to learn more about this, or change the content of the email, or the method by which they are sent, checkout the email / SMS delivery section in the recipe docs: - [Email delivery configuration](/docs/platform-configuration/email-delivery) - [SMS delivery configuration](/docs/platform-configuration/sms-delivery) # Additional Verification - Multi Factor Authentication - OTP - OTP for specific users Source: https://supertokens.com/docs/additional-verification/mfa/email-sms-otp/otp-for-opt-in-users :::important Before reading the below, please first go through the setup for [OTP for all users](./otp-for-all-users) to understand the basics of how MFA with OTP works, and then come back here. ::: This page shows how to implement an MFA policy that requires certain users to do the OTP challenge via email or SMS. You can decide which users based on any criteria. For example: - Only users that have an `admin` role require to do OTP; OR - Only users that have enabled OTP on their account require to do OTP; OR - Only users that have a paid account require to do OTP. Whatever the criteria is, the steps to implementing this type of a flow is the same. :::note Assume that the first factor is email password or social login, but the same set of steps applies to other first factor types as well. ::: ## Single tenant setup ### Backend setup #### Example 1: Only enable OTP for users that have an `admin` role To start with, configure the backend in the following way: ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ Session.init(), UserRoles.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), // highlight-start Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE" }), AccountLinking.init({ shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { if (session === undefined) { // we do not want to do first factor account linking by default. To enable that, // please see the automatic account linking docs in the recipe docs for your first factor. return { shouldAutomaticallyLink: false }; } if (user === undefined || session.getUserId() === user.id) { // if it comes here, it means that a session exists, and we are trying to link the // newAccountInfo to the session user, which means it's an MFA flow, so we enable // linking here. return { shouldAutomaticallyLink: true, shouldRequireVerification: false } } return { shouldAutomaticallyLink: false }; } }), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id) if (roles.roles.includes("admin")) { // we only want otp-email for admins return [MultiFactorAuth.FactorIds.OTP_EMAIL] } else { // no MFA for non admin users. return [] } } } } } // highlight-end }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, accountlinking from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, MFARequirementList, ) from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink, ) from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List, Optional, Union from supertokens_python.recipe.userroles.asyncio import get_roles_for_user async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any], ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is None: # We do not want to do first factor account linking by default. # To enable that, please see the automatic account linking docs # in the recipe docs for your first factor. return ShouldNotAutomaticallyLink() if user is None or session.get_user_id() == user.id: # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. return ShouldAutomaticallyLink(should_require_verification=False) return ShouldNotAutomaticallyLink() def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # Get roles for the user roles = await get_roles_for_user(tenant_id, (await user()).id) if "admin" in roles.roles: # We only want OTP_EMAIL for admins return [FactorIds.OTP_EMAIL] else: # No MFA for non-admin users return [] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), accountlinking.init( should_do_automatic_account_linking=should_do_automatic_account_linking ), ], ) ``` Override the `getMFARequirementsForAuth` function to indicate that `otp-email` applies only to users with the `admin` role. You can also have any other criteria here. #### Example 2: Ask for OTP only for users that have enabled OTP on their account To start with, configure the backend in the following way: ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), // highlight-start Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE", override: { apis: (oI) => { return { ...oI, consumeCodePOST: async function (input) { let response = await oI.consumeCodePOST!(input); if (response.status === "OK" && input.session !== undefined) { // We do this only if a session exists, which means that it's not being called for first factor login. // OTP challenge completed successfully. We save that this user has enabled otp-email in the user metadata. // The multifactorauth recipe will pick this value up next time the user is trying to login, and // ask them to enter the OTP code. await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(input.session.getUserId(), MultiFactorAuth.FactorIds.OTP_EMAIL); } return response; } } } } }), AccountLinking.init({ shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { if (session === undefined) { // we do not want to do first factor account linking by default. To enable that, // please see the automatic account linking docs in the recipe docs for your first factor. return { shouldAutomaticallyLink: false }; } if (user === undefined || session.getUserId() === user.id) { // if it comes here, it means that a session exists, and we are trying to link the // newAccountInfo to the session user, which means it's an MFA flow, so we enable // linking here. return { shouldAutomaticallyLink: true, shouldRequireVerification: false } } return { shouldAutomaticallyLink: false }; } }), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ] }) // highlight-end ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, accountlinking from supertokens_python.recipe.multifactorauth.types import ( FactorIds, ) from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink, ) from supertokens_python.types import User from typing import Dict, Any, Optional, Union from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless.interfaces import ( RecipeInterface, ConsumeCodeOkResult, ) from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, ) async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any], ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is None: # We do not want to do first factor account linking by default. # To enable that, please see the automatic account linking docs # in the recipe docs for your first factor. return ShouldNotAutomaticallyLink() if user is None or session.get_user_id() == user.id: # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. return ShouldAutomaticallyLink(should_require_verification=False) return ShouldNotAutomaticallyLink() def override_functions(original_implementation: RecipeInterface): original_consume_code = original_implementation.consume_code async def consume_code( pre_auth_session_id: str, user_input_code: Union[str, None], device_id: Union[str, None], link_code: Union[str, None], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, user_context: Dict[str, Any], ): response = await original_consume_code( pre_auth_session_id, user_input_code, device_id, link_code, session, should_try_linking_with_session_user, tenant_id, user_context, ) if isinstance(response, ConsumeCodeOkResult) and session is not None: await add_to_required_secondary_factors_for_user( session.get_user_id(), FactorIds.OTP_EMAIL ) return response original_implementation.consume_code = consume_code return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ passwordless.init( contact_config=passwordless.ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE", override=passwordless.InputOverrideConfig(functions=override_functions), ), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], ), accountlinking.init( should_do_automatic_account_linking=should_do_automatic_account_linking ), ], ) ``` - Initialize the multi-factor auth recipe here without any override to `getMFARequirementsForAuth`. The default implementation of this function already checks what factors a user has enabled and returns those. All that is needed is to mark `otp-email` as enabled for a user as soon as they have completed the OTP challenge successfully. This happens in the `consumeCodePOST` API override as shown above. Once the code is consumed successfully, mark the `otp-email` factor as enabled for the user, and the next time they login, they will be asked to complete the OTP challenge. - Notice that before calling `addToRequiredSecondaryFactorsForUser`, check if there is an input session or not. Only call `addToRequiredSecondaryFactorsForUser` function if there is a session which indicates that the user has finished some first factor already. In both of the examples above, notice that the Passwordless recipe initializes in the `recipeList`. In this example, only email-based OTP is enabled, set the `contactMethod` to `EMAIL` and `flowType` to `USER_INPUT_CODE` (that is, OTP). If instead, you want to use phone SMS-based OTP, set the contact method to `PHONE`. If you want to give users both options, or for some users use email, and for others use phone, set `contactMethod` to `EMAIL_OR_PHONE`. We have also enabled the account linking feature since it's required for MFA to work. The above enables account linking for second factor only, but if you also want to enable it for first factor, see [this section](/docs/post-authentication/account-linking/automatic-account-linking). Notice that `shouldRequireVerification: false` configures account linking. It means that the second factor can link to the first factor even though the first factor is not verified. If you want to do email verification of the first factor before setting up the second factor (for example if the first factor is email password, and the second is phone OTP), then set this boolean to `true`, and also init the email verification recipe on the frontend and backend in `REQUIRED` mode. Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload looks like this (for those that require OTP): ```json { "st-mfa": { "c": { "emailpassword": 1702877939, }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished otp-email, the payload looks like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "otp-email": 1702877999 }, "v": true } } ``` Indicating that the user has finished all required factors, and should access the app. :::caution If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. ::: ### Frontend setup This consists of two parts: - Configuring the frontend to show the OTP challenge UI when required during login / sign up - Allowing users to enable / disable OTP challenge on their account via the settings page (If you are following Example 2 from above). The first part is identical to the steps mentioned in [this section](./otp-for-all-users#frontend-setup), please follow that. The second part, which is only applicable in case you want to allow users to enable / disable OTP themselves, can be done by creating the following flow on your frontend: - When the user navigates to their settings page, you can show them if OTP challenge is active or not. - If enabled, you can allow them to disable it, or vice versa. To know if the user has enabled OTP, you can make an API your backend which calls the following function: ```ts async function isOTPEmailEnabledForUser(userId: string) { let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId) return factors.includes(MultiFactorAuth.FactorIds.OTP_EMAIL) } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds async def is_otp_email_factor_enabled_for_user(user_id: str) -> bool: factors = await get_required_secondary_factors_for_user(user_id, {}) return FactorIds.OTP_EMAIL in factors ``` If the user wants to enable or disable otp-email for them, you can make an API on your backend which calls the following function: ```ts async function enableMFAForUser(userId: string) { await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.OTP_EMAIL) } async function disableMFAForUser(userId: string) { await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.OTP_EMAIL) } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, remove_from_required_secondary_factors_for_user, ) from supertokens_python.recipe.multifactorauth.types import FactorIds async def enable_mfa_for_user(user_id: str) -> None: await add_to_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL) async def disable_mfa_for_user(user_id: str) -> None: await remove_from_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL) ``` :::note If instead you want to work with `otp-phone`, you can replace `otp-email` with `otp-phone` in the above snippets. Also make sure that the `contactMethod` configures to `PHONE` in the Passwordless recipe on the frontend (for pre-built UI) and backend. ::: ## Multi tenant setup ### Backend setup A user can be a part of multiple tenants. If you want OTP to be active for a specific user across all the tenants that they are a part of, the steps are the same as in the [Backend setup](#backend-setup) section above. However, if you want OTP to be active for a specific user, for a specific tenant (or a subset of tenants that the user is a part of), then additional logic must be added to the `getMFARequirementsForAuth` function override. Modifying the example code from the [Backend setup](#backend-setup) section above: #### Example 1: Only enable OTP for users that have an `admin` role ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ Session.init(), UserRoles.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), // highlight-next-line Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE" }), AccountLinking.init({ shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { if (session === undefined) { // we do not want to do first factor account linking by default. To enable that, // please see the automatic account linking docs in the recipe docs for your first factor. return { shouldAutomaticallyLink: false }; } if (user === undefined || session.getUserId() === user.id) { // if it comes here, it means that a session exists, and we are trying to link the // newAccountInfo to the session user, which means it's an MFA flow, so we enable // linking here. return { shouldAutomaticallyLink: true, shouldRequireVerification: false } } return { shouldAutomaticallyLink: false }; } }), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id) // highlight-next-line if (roles.roles.includes("admin") && (await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.OTP_EMAIL)) { // we only want otp-email for admins return [MultiFactorAuth.FactorIds.OTP_EMAIL] } else { // no MFA for non admin users. return [] } } } } } }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, accountlinking, passwordless from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, MFARequirementList, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink, ) from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List, Optional, Union from supertokens_python.recipe.userroles.asyncio import get_roles_for_user async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any], ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is None: # We do not want to do first factor account linking by default. # To enable that, please see the automatic account linking docs # in the recipe docs for your first factor. return ShouldNotAutomaticallyLink() if user is None or session.get_user_id() == user.id: # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. return ShouldAutomaticallyLink(should_require_verification=False) return ShouldNotAutomaticallyLink() def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # Get roles for the user roles = await get_roles_for_user(tenant_id, (await user()).id) if ( "admin" in roles.roles and FactorIds.OTP_EMAIL in await required_secondary_factors_for_tenant() ): # We only want OTP_EMAIL for admins return [FactorIds.OTP_EMAIL] else: # No MFA for non-admin users return [] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" ), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), accountlinking.init( should_do_automatic_account_linking=should_do_automatic_account_linking ), ], ) ``` - The implementation of `shouldRequireOTPEmailForTenant` is entirely up to you. #### Example 2: Ask for OTP only for users that have enabled OTP on their account ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE", override: { apis: (oI) => { return { ...oI, consumeCodePOST: async function (input) { let response = await oI.consumeCodePOST!(input); if (response.status === "OK" && input.session !== undefined) { // We do this only if a session exists, which means that it's not being called for first factor login. // OTP challenge completed successfully. We save that this user has enabled otp-email in the user metadata. // The multifactorauth recipe will pick this value up next time the user is trying to login, and // ask them to enter the OTP code. await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(input.session.getUserId(), MultiFactorAuth.FactorIds.OTP_EMAIL); } return response; } } } } }), AccountLinking.init({ shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { if (session === undefined) { // we do not want to do first factor account linking by default. To enable that, // please see the automatic account linking docs in the recipe docs for your first factor. return { shouldAutomaticallyLink: false }; } if (user === undefined || session.getUserId() === user.id) { // if it comes here, it means that a session exists, and we are trying to link the // newAccountInfo to the session user, which means it's an MFA flow, so we enable // linking here. return { shouldAutomaticallyLink: true, shouldRequireVerification: false } } return { shouldAutomaticallyLink: false }; } }), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { if ((await input.requiredSecondaryFactorsForUser).includes(MultiFactorAuth.FactorIds.OTP_EMAIL)) { if ((await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.OTP_EMAIL)) { return [MultiFactorAuth.FactorIds.OTP_EMAIL] } } // no otp-email required for input.user, with the input.tenant. return [] } } } } // highlight-end }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, accountlinking, passwordless from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, MFARequirementList, ) from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink, ) from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List, Optional, Union from supertokens_python.recipe.passwordless.interfaces import ( APIInterface, APIOptions, ConsumeCodePostOkResult, ) async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any], ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is None: # We do not want to do first factor account linking by default. # To enable that, please see the automatic account linking docs # in the recipe docs for your first factor. return ShouldNotAutomaticallyLink() if user is None or session.get_user_id() == user.id: # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. return ShouldAutomaticallyLink(should_require_verification=False) return ShouldNotAutomaticallyLink() def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: if FactorIds.OTP_EMAIL in await required_secondary_factors_for_user(): if FactorIds.OTP_EMAIL in await required_secondary_factors_for_tenant(): return [FactorIds.OTP_EMAIL] # no otp-email required for input.user, with the input.tenant. return [] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation def passwordless_override(original_implementation: APIInterface): original_consume_code_post = original_implementation.consume_code_post async def consume_code_post( pre_auth_session_id: str, user_input_code: Union[str, None], device_id: Union[str, None], link_code: Union[str, None], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): response = await original_consume_code_post( pre_auth_session_id, user_input_code, device_id, link_code, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) if isinstance(response, ConsumeCodePostOkResult) and session is not None: await add_to_required_secondary_factors_for_user( session.get_user_id(), FactorIds.OTP_EMAIL ) return response original_implementation.consume_code_post = consume_code_post return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE", override=passwordless.InputOverrideConfig(apis=passwordless_override), ), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), accountlinking.init( should_do_automatic_account_linking=should_do_automatic_account_linking ), ], ) ``` An override for `getMFARequirementsForAuth` is available, which checks if otp-email is active for the user, and also considers the `tenantId` to decide if this user should go through the otp-email flow while logging into this tenant. The implementation of `shouldRequireOTPEmailForTenant` is entirely up to you. ### Frontend setup The frontend setup is identical to the [frontend setup](#frontend-setup) section above. ## Protecting frontend and backend routes See the section on [protecting frontend and backend routes](../protect-routes). ## Email / SMS sending and design By default, the email template used for otp-email login is [as shown here](https://github.com/SuperTokens/email-sms-templates?tab=readme-ov-file#otp-login), and the default SMS template is [as shown here](https://github.com/SuperTokens/email-sms-templates?tab=readme-ov-file#otp-login-1). The method for sending them is via an email and SMS sending service that is available. If you would like to learn more about this, or change the content of the email, or the method by which they send, checkout the email / SMS delivery section in the recipe docs: - [Email delivery configuration](/docs/platform-configuration/email-delivery) - [SMS delivery configuration](/docs/platform-configuration/sms-delivery) # Additional Verification - Multi Factor Authentication - Implement WebAuthn as a secondary factor Source: https://supertokens.com/docs/additional-verification/mfa/webauthn-secondary-factor-setup ## Overview This guide shows how to implement an MFA policy that requires all users to use WebAuthn before they get access to your application. ## Before you start The tutorial assumes that the first factor is email password or social login, but the same set of steps are applicable for other first factor types. ## Steps ### 1. Configure the backend To start with, we configure the backend in the following way: ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), // highlight-next-line webauthn.init(), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { // Change this implementation if you want to require webauthn only for specific users return [MultiFactorAuth.FactorIds.WEBAUTHN] } } } } // highlight-end }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, webauthn from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, MFARequirementList, ) from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # Change this implementation if you want to require webauthn only for specific users return [FactorIds.WEBAUTHN] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ webauthn.init(), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` The MFA recipe override is required to indicate that `webauthn` must be completed before the user can access the app. Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished `webauthn`, the payload will look like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "webauthn": 1702877999 }, "v": true } } ``` Indicating that the user has finished all required factors, and should be allowed to access the app. In a multi tenancy setup, you may want to enable WebAuthn for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the [single tenant setup](#single-tenant-setup) section above, so in this section, we will focus on enabling WebAuthn for all users within specific tenants. To start, we will initialise the WebAuthn and the MultiFactorAuth recipes in the following way: ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), // highlight-start webauthn.init(), MultiFactorAuth.init() // highlight-end ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, webauthn init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", # type: ignore recipe_list=[ webauthn.init(), multifactorauth.init(), ], ) ``` Unlike the single tenant setup, we do not provide any config to the `MultiFactorAuth` recipe cause all the necessary configuration will be done on a tenant level. To configure WebAuthn requirement for a tenant, we can call the following API: ```tsx async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ], requiredSecondaryFactors: [MultiFactorAuth.FactorIds.WEBAUTHN] }); if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } } ``` - In the above, we set the `firstFactors` to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. - We set the `requiredSecondaryFactors` to `["webauthn"]` to indicate that WebAuthn is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds async def create_new_tenant(): resp = await create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD], required_secondary_factors=[FactorIds.WEBAUTHN], ) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds def create_new_tenant(): resp = create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD], required_secondary_factors=[FactorIds.WEBAUTHN], ) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished `webauthn`, the payload will look like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "webauthn": 1702877999 }, "v": true } } ``` Indicating that the user has finished all required factors, and should be allowed to access the app. ### 2. Configure the frontend We start by modifying the `init` function call on the frontend like so: ```tsx supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes.. // highlight-start webauthn.init(), MultiFactorAuth.init(), Multitenancy.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getTenantId: async (context) => { return "TODO" } } } } }) // highlight-end ] }) ``` You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: This change is in your auth route config. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes.. // highlight-start supertokensUIWebAuthn.init(), supertokensUIMultiFactorAuth.init(), supertokensUIMultitenancy.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getTenantId: async (context) => { return "TODO" } } } } }) // highlight-end ] }) ``` This change goes in the `supertokens-web-js` SDK config at the root of your application: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... // highlight-start MultiFactorAuth.init(), WebAuthn.init() // highlight-end ], }); ``` ```tsx supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes.. // highlight-start webauthn.init(), MultiFactorAuth.init({ firstFactors: [ MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY ] }) // highlight-end ] }) ``` You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: This change is in your auth route config. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes.. // highlight-start supertokensUIWebAuthn.init(), supertokensUIMultiFactorAuth.init({ firstFactors: [ supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY ] }) // highlight-end ] }) ``` This change goes in the `supertokens-web-js` SDK config at the root of your application: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... // highlight-start MultiFactorAuth.init(), WebAuthn.init() // highlight-end ], }); ``` On the frontend, the `MultiFactorAuth` recipe intialization only requires the first factors to be configured. The secondary factors will be determined based on a requiest to the backend. Add the WebAuthn pre-built UI to render the SuperTokens component: ```tsx function App() { return (
// highlight-start {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [/* ... */ WebauthnPreBuiltUI, MultiFactorAuthPreBuiltUI])} // highlight-end // ... other routes
); } ```
```tsx function App() { // highlight-start if (canHandleRoute([/* ... */ WebauthnPreBuiltUI, MultiFactorAuthPreBuiltUI])) { return getRoutingComponent([/* ... */ WebauthnPreBuiltUI, MultiFactorAuthPreBuiltUI]) } // highlight-end return ( {/*Your app*/} ); } ```
:::success This step is not required for non React apps, since all the pre-built UI components are already added into the bundle. :::
We start by initialising the MFA and WebAuthn recipe on the frontend like so: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... // highlight-start MultiFactorAuth.init(), WebAuthn.init() // highlight-end ], }); ``` ```tsx supertokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... // highlight-start supertokensMultiFactorAuth.init(), supertokensWebAuthn.init() // highlight-end ], }); ``` :::success This step is not applicable for mobile apps. Please continue reading. ::: After the first factor login, you should start by checking the access token payload and see if the MFA claim's `v` boolean is `false`. 'If it's not, then you can redirect the user to the application page. If it's `false`, the frontend then needs to [call the MFA endpoint](/docs/references/fdi/mfa/put-mfa-info) to get information about which factor the user should be asked to complete next. Based on the initial backend configuration, the `next` array will contain `["webauthn"]`. To complete the secondary factor you need to take into account if the users has previously configured a passkey or not. You can determine this by checking if the `alreadySetup` array contains `"webauthn"`. #### Sign up flow ```ts async function secondFactorSignUp(email: string, userContext: Record) { const response = await Webauthn.registerCredentialWithSignUp({ email, shouldTryLinkingWithSessionUser: true, userContext, }); return response.status === "OK"; } ``` ```ts async function secondFactorSignUp(email: string, userContext: Record) { const response = await recipeImplementation.registerCredentialWithSignUp({ email, shouldTryLinkingWithSessionUser: true, userContext, }); return response.status === "OK"; } ``` Support for this flow is not available in the mobile SDK. You will have to call the [backend API](/docs/references/fdi/introduction) directly. First, call the [**Register WebAuthn Credential**](/docs/references/fdi/webauthn/post-webauthn-credential) endpoint to register the passkey. Afterwards call the [**Sign Up with WebAuthn**](/docs/references/fdi/webauthn/post-webauthn-signup) to complete the second factor sign up process. #### Sign in flow ```ts async function secondFactorSignUp(userContext: Record) { const response = await recipeImplementation.authenticateCredentialWithSignIn({ shouldTryLinkingWithSessionUser: true, userContext, }); return response.status === "OK"; } ``` ```ts async function secondFactorSignUp(userContext: Record) { const response = await recipeImplementation.authenticateCredentialWithSignIn({ shouldTryLinkingWithSessionUser: true, userContext, }); return response.status === "OK"; } ``` Support for this flow is not available in the mobile SDK. You will have to call the [backend API](/docs/references/fdi/introduction) directly. Call the [**Sign in with WebAuthn**](/docs/references/fdi/webauthn/post-webauthn-signin) endpoint to complete the secondary factor flow. That's it! :tada: Based on this configuration, users first access the authentication form which shows the `emailpassword` and `thirdparty` options. After first factor completion, they access the WebAuthn form to finalize the authentication attempt. # Additional Verification - Multi Factor Authentication - Implement recovery codes Source: https://supertokens.com/docs/additional-verification/mfa/backup-codes ## Overview Backup codes is one of the ways in which end users can recover their account in case they lose their second factor device. At the moment, SuperTokens does not have an in-built implementation for backup codes, however, you can customize the SDKs to add it. :::info Note [Here is an example](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes) of how you can implement backup codes in your application if you are using the pre-built UI. ::: This guide shows you how to implement the following flow: - After MFA setup users can generate backup codes. - The backup code associates with the userID in the `UserMetadata` JSON. - When a user wants to use their backup code, the application shows them a UI to enter the code. This calls an API that verifies the backup code and adds a flag in their session, indicating that they have correctly supplied a backup code. - After that, the flag allows users to create a new MFA device, even if they already have one registered on their account. User can then go about adding a new device and completing MFA using that. ## Before you start These instructions assume that you already have some knowledge of MFA. If you are not familiar with terms like authentication factors and challenges, please go through the [MFA concepts page](/docs/additional-verification/mfa/important-concepts). The guide below focuses on Time-based One-Time Password (TOTP) as a second factor, but you can implement something similar for Passwordless as well. ## Steps ### 1. Add an API to generate backup codes on the backend Here is an [example API](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/index.ts#L52) for how you can create a recovery code. In here, you create a secure random string and save the hashed version in the user metadata JSON. This API returns the plain text recovery code to the frontend to display to the user. ### 2. Allow users to generate a backup code when they finish MFA setup After the user has successfully set up their second factor (during sign up or during recovery process), they navigate to a page which shows them their backup code. The user can automatically redirect to this page (once they have completed their MFA setup) by adding a claim validator on the frontend. This enforces that the user always has a backup code associated with their account. Even if they consume the code in the future, this validator fails and redirects them to the create new backup code screen. The idea here is to modify the access token payload on the backend to add a boolean value to it. This value is `true` if the user already has a backup code with their account, else it's `false`. The frontend claim validator fails if the value is `false` and redirects them to the UI for creating a backup code. You can see this validator implementation on the frontend in the [recoveryCodeExistsClaim.ts file](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/recoveryCodeExistsClaim.ts). This validator adds to the [Session.init on the frontend](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/config.tsx#L56) that it runs each time you protect a route with ``. It is also necessary to add a claim validator on the backend to add the boolean value to the access token payload. You can see the claim validator's implementation in the [recoveryCodeExistsClaim.ts file](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/recoveryCodeExistsClaim.ts). This validator appears in a few places: - [When creating a new backup code](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/index.ts#L61). - [When consuming an existing backup code](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L55). - [When creating a new session](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L127). Once on the page, the UI calls the API created in the previous step to create a new recovery code for the user. Note that calling this API replaces the older recovery code, but since it's all custom, you can change the logic. [Here is an example](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/CreateRecoveryCode/index.tsx) of how to implement this page. For custom UI, the UI for where and how you show the recovery code page is up to you. It is advisable to show the user this page post sign up, or whenever they create a new MFA device successfully. The user can automatically redirect to this page (once they have completed their MFA setup) by adding a claim validator on the frontend. This enforces that the user always has a backup code associated with their account. Even if they consume the code in the future, this validator fails and redirects them to the create new backup code screen. The idea here is to modify the access token payload on the backend to add a boolean value to it. This value is `true` if the user already has a backup code with their account, else it's `false`. The frontend claim validator fails if the value is `false` and redirects them to the UI for creating a backup code. You can see this validator implementation on the frontend in the [recoveryCodeExistsClaim.ts file](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/recoveryCodeExistsClaim.ts). This validator adds to the [Session.init on the frontend](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/config.tsx#L56) that it runs each time you protect a route with `await Session.validateClaims` function call. It is also necessary to add a claim validator on the backend to add the boolean value to the access token payload. You can see the claim validator's implementation in the [recoveryCodeExistsClaim.ts file](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/recoveryCodeExistsClaim.ts). This validator appears in a few places: - [When creating a new backup code](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/index.ts#L61). - [When consuming an existing backup code](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L55). - [When creating a new session](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L127). Once on the page, the UI calls the API created in the previous step to create a new recovery code for the user. Note that calling this API replaces the older recovery code, but since it's all custom, you can change the logic. [Here is an example](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/CreateRecoveryCode/index.tsx) of how to implement this page. ### 3. Show how to use backup codes on the MFA challenge UI You can achieve this by creating a "Lost device?" button in the pre-built UI that asks the user to enter the Time-based One-Time Password (TOTP) challenge. Once they click on this, users redirect to a page where they can enter their backup code. After verification, they further redirect to the create a new Time-based One-Time Password (TOTP) device page. [Here is how you can override the pre-built UI](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/App.tsx#L19) to display the "Lost device?" button. [Here is an example implementation](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/RecoveryCode/index.tsx) of a page which asks the user to enter their backup code. It then calls an API (see next step) to check if the code is correct or not. You should make a UI that asks the user to enter their backup code and call the API to verify it and mark it as "in use" (see next step). You want to give the option for users to enter their backup code when asked for the MFA challenge. ### 4. Modify the user's session to mark that they have verified their backup code On the backend, you set up an API that accepts the recovery code entered by the user. It checks that it matches the hashed version stored in the user's metadata JSON. If it does, it marks as "in use" in the user's session payload. You achieve this by saving the `recoverCodeHash` in the session payload, which is then checked in the next step to force enable Time-based One-Time Password (TOTP) device creation. On the frontend, once this API returns a success, the user should navigate to the create a new Time-based One-Time Password (TOTP) device screen. You can see how this process completes in the [index.tsx file](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/RecoveryCode/index.tsx#L20). On the frontend, once this API returns a success, the user should navigate to the create a new Time-based One-Time Password (TOTP) device screen. ### 5. Force users to setup a new device Here are the steps: - Force enabling Time-based One-Time Password (TOTP) device creation if the `recoveryCodeHash` is in the user's session's access token payload. - Deleting the recovery code from the user's metadata JSON once they have consumed it to create a new device. - Redirecting the user to the page that shows their new recovery code after they have set up their new device. You can achieve the first step by overriding [this function](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L68) on the backend. By default, the `assertAllowedToSetupFactorElseThrowInvalidClaimError` function throws an error if a Time-based One-Time Password (TOTP) device already exists. If the user tries to set up a new TOTP device during the sign-in process, it is for security reasons. However, this modifies to check if the access token payload contains the `recoveryCodeHash` and that it matches the one in the user metadata JSON. If it does, you allow new device setup, since it is known that the user had previously entered their recovery code successfully. During the Time-based One-Time Password (TOTP) device setup, users call [this API](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L45) from the frontend. If this API succeeds, then it means that a new TOTP device has been created for the user and they have completed the TOTP challenge for the current session. On success, the old recovery code removes from the metadata in case the session has the `recoveryCodeHash` set. This way, the old recovery code is no longer usable. Finally, after successfully creating a new Time-based One-Time Password (TOTP) device, the frontend should redirect the user to the page which shows the new recovery code for the user. Refer to step 2 above. --- # Additional Verification - Multi Factor Authentication - Implement step-up authentication Source: https://supertokens.com/docs/additional-verification/mfa/step-up-auth ## Overview Step-up authentication enforces the user to complete an authentication challenge before navigating to a page, or before doing a specific action. You can implement it with **SuperTokens** as full page navigation, or as popups on the current page. ## Before you start These instructions assume that you already have some knowledge of MFA. If you are not familiar with terms like authentication factors and challenges, please go through the [MFA concepts page](/docs/additional-verification/mfa/important-concepts). ### Prerequisites Step-up authentication supports the following factors: - `TOTP` - `WebAuthn/Passkeys` - Password (available only for custom UI) - Email or SMS `OTP` If you are using **OAuth2** in your configuration, step-up authentication is not supported at the moment. ## Steps ### 1. Add the backend validators To protect sensitive APIs with step up auth, you need to check that the user has completed the required auth challenge within a certain amount of time. If they haven't, you should return a `403` to the frontend which highlights which factor is necessary. The frontend can then consume this and show the auth challenge to the user. ```tsx let app = express(); app.post( "/update-blog", verifySession(), async (req: SessionRequest, res) => { let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }] }) } // continue with API logic... } ); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }] }) } // continue with API logic... } }) ``` ```tsx let fastify = Fastify(); fastify.post("/update-blog", { preHandler: verifySession(), }, async (req: SessionRequest, res) => { let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }] }) } // continue with API logic... }); ``` ```tsx async function updateBlog(awsEvent: SessionEvent) { let mfaClaim = await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }] }) } // continue with API logic... }; exports.handler = verifySession(updateBlog); ``` ```tsx let router = new KoaRouter(); router.post("/update-blog", verifySession(), async (ctx: SessionContext, next) => { let mfaClaim = await ctx.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }] }) } // continue with API logic... }); ``` ```tsx class Example { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/update-blog") @intercept(verifySession()) @response(200) async handler() { let mfaClaim = await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }] }) } // continue with API logic... } } ``` ```tsx // highlight-start export default async function example(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res ) let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again await superTokensNextWrapper( async (next) => { throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }] }) }, req, res ) } // continue with API logic... } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } let mfaClaim = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again const error = new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }] }) return NextResponse.json(error, { status: 403 }); } // continue with API logic... return NextResponse.json({}) }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { @Post('example') @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise { let mfaClaim = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }] }) } // continue with API logic... return true; } } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python ### 2. Prevent factor setup during step-up authentication By default, SuperTokens allows a factor setup, such as creating a new TOTP device, as long as the user has a session and has completed all the MFA factors required during login. This opens up a security issue when it comes to completing step up auth. Consider the following scenario: - The user has logged in and completed TOTP - After 5 minutes, the user tries to do a sensitive action and the API for that fails with a 403 (cause of the check in step 1, above). - The user sees the TOTP challenge on the frontend. However, instead of completing that, they call the create TOTP device API which would succeed and then use the new TOTP device to complete the factor challenge required for the API. This allows someone malicious to bypass step up auth. To prevent this, override one of the MFA recipe functions on the backend. This enforces that the factor setup can only happen if the user is not in a step-up auth state. ```ts supertokens.init({ supertokens: { connectionURI: "..." }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "..." }, recipeList: [ MultiFactorAuth.init({ firstFactors: [/*...*/], override: { functions: (originalImplementation) => { return { ...originalImplementation, // highlight-start assertAllowedToSetupFactorElseThrowInvalidClaimError: async (input) => { await originalImplementation.assertAllowedToSetupFactorElseThrowInvalidClaimError(input); let claimValue = await input.session.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (claimValue === undefined || !claimValue.v) { return } // if the above did not throw, it means that the user has logged in and has completed all the required // factors for login. So now we check specifically for the step up auth case: if (input.factorId === MultiFactorAuth.FactorIds.TOTP && (await input.factorsSetUpForUser).includes(MultiFactorAuth.FactorIds.TOTP)) { // this is an example of checking for totp, but you can also use other factor IDs. const totpCompletedTime = claimValue.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000 * 60 * 5)) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }] }) } } } // highlight-end } } } }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python async function redirectToTotpSetupScreen() { MultiFactorAuth.redirectToFactor({ factorId: "totp", stepUp: true, redirectBack: true, }) } ``` - In the snippet above, redirect to the [TOTP factor setup screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/totp-mfa--device-setup). Set the `stepUp` argument to `true` otherwise the MFA screen would detect that the user has already completed basic MFA requirements and would not show the verification screen. Set the `redirectBack` argument to `true` since the intention is to redirect back to the current page after the user has finished setting up the device. - You can also redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` if you don't want to use the above function. To add a new device, you can redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` from your settings page. This shows the [TOTP factor setup screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/totp-mfa--device-setup) to the user. The `redirectToPath` query parameter also tells the SDK to redirect the user back to the current page after they have finished creating the device. #### Show the factor in a popup Checkout [the documentation](/docs/additional-verification/mfa/embed-the-prebuilt-ui) for embedding the pre-built UI factor components in a page or a popup. You can check for this structure and the `factorId` to decide what factor to show on the frontend. ### 4. Check for step-up authentication on page navigation Sometimes, you may want to ask users to complete step up auth before displaying a page on the frontend. This is a different scenario than the above steps cause. Here, you do not want to rely on an API call to fail. Instead, you want to check for the step-up auth condition before rendering the page itself. To do this, read the access token payload on the frontend and check the completed time of the factor of interest before rendering the page. If the completed time is older than 5 minutes (as an example), redirect the user to the factor challenge page. ```tsx const VerifiedRoute = (props: React.PropsWithChildren) => { return ( {props.children} ); } function InvalidClaimHandler(props: React.PropsWithChildren) { let claimValue = useClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (claimValue.loading) { return null; } let totpCompletedTime = claimValue.value?.c[MultiFactorAuth.FactorIds.TOTP] if (totpCompletedTime === undefined || totpCompletedTime < (DateProviderReference.getReferenceOrThrow().dateProvider.now() - 1000 * 60 * 5)) { return
You need to complete TOTP before seeing this page. Please click here to finish to proceed.
} // the user has finished TOTP, so we can render the children return
{props.children}
; } ``` - Check if the user has completed TOTP within the last 5 minutes or not. If not, show a message to the user, and ask them to complete TOTP. - Notice that the `DateProviderReference` class exported by SuperTokens replaces `Date.now()`. This accounts for any clock skew that may exist between the frontend and the backend server.
```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims(); if (validationErrors.length === 0) { // since all default claim validators have passed, we now check for if the user has finished TOTP // within the last 5 mins let mfaClaimValue = await Session.getClaimValue({ claim: MultiFactorAuthClaim }); let totpCompletedTime = mfaClaimValue?.c["totp"]; if (totpCompletedTime === undefined || totpCompletedTime < (DateProviderReference.getReferenceOrThrow().dateProvider.now() - 1000 * 60 * 5)) { // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. return false; } return true; } else { // handle other validation failure events... } } // a session does not exist, or email is not verified return false } ``` - In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. If that passes, it means all the default claim validators have passed (checks applied to all routes in general), and then perform the step-up auth check. - For checking for step-up auth, get the MFA claim value from the session and then check if TOTP completed within the last 5 minutes. Only if it did, return true, else return false. - Notice that the `DateProviderReference` class exported by SuperTokens replaces `Date.now()`. This accounts for any clock skew that may exist between the frontend and the backend server.
```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims(); if (validationErrors.length === 0) { // since all default claim validators have passed, we now check for if the user has finished TOTP // within the last 5 mins let mfaClaimValue = await Session.getClaimValue({ claim: MultiFactorAuthClaim }); let totpCompletedTime = mfaClaimValue?.c["totp"]; if (totpCompletedTime === undefined || totpCompletedTime < (DateProviderReference.getReferenceOrThrow().dateProvider.now() - 1000 * 60 * 5)) { // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. return false; } return true; } else { // handle other validation failure events... } } // a session does not exist, or email is not verified return false } ``` ```tsx async function shouldLoadRoute(): Promise { if (await supertokensSession.doesSessionExist()) { let validationErrors = await supertokensSession.validateClaims(); if (validationErrors.length === 0) { // since all default claim validators have passed, we now check for if the user has finished TOTP // within the last 5 mins let mfaClaimValue = await supertokensSession.getClaimValue({ claim: supertokensMultiFactorAuth.MultiFactorAuthClaim }); let totpCompletedTime = mfaClaimValue?.c["totp"]; if (totpCompletedTime === undefined || totpCompletedTime < (supertokensDateProviderReference.DateProviderReference.getReferenceOrThrow().dateProvider.now() - 1000 * 60 * 5)) { // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. return false; } return true; } else { // handle other validation failure events... } } // a session does not exist, or email is not verified return false } ``` - In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. If that passes, it means all the default claim validators have passed (checks applied to all routes in general), and then perform the step-up auth check. - For checking for step-up auth, get the MFA claim value from the session and then check if TOTP completed within the last 5 minutes. Only if it did, return true, else return false. - Notice that the `DateProviderReference` class exported by SuperTokens replaces `Date.now()`. This accounts for any clock skew that may exist between the frontend and the backend server. ```tsx async function checkIfMFAIsCompleted() { if (await SuperTokens.doesSessionExist()) { // highlight-start let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; if (isMFACompleted) { let completedFactors = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].c; if (completedFactors["totp"] === undefined || completedFactors["totp"] < (Date.now() - 1000*60*5)) { // user has not finished TOTP MFA in the last 5 minutes } } else { // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } // highlight-end } } ``` ```kotlin } } } catch (e: Exception) { // Handle exceptions such as ClassCastException or JSONException } } } ``` ```swift if isMFACompleted { // All required factors for MFA have been completed if let mfaCompletedFactors = mfaObject["c"] as? [String: Any], let totpTime = mfaCompletedFactors["totp"] as? Double { // Corrected unwrapping of mfaCompletedFactors and casting of totpTime if totpTime < (Date().timeIntervalSince1970 - 1000*60*5) { // user has not finished TOTP MFA in the last 5 minutes } } } else { // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } } } } ``` ```dart Future checkIfMFAIsCompleted() async { var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); if (accessTokenPayload.containsKey("st-mfa")) { Map mfaObject = accessTokenPayload["st-mfa"]; if (mfaObject.containsKey("v")) { bool isMFACompleted = mfaObject["v"] as bool; // Casting to bool if (isMFACompleted) { // All required factors for MFA have been completed Map mfaCompletedFactors = mfaObject["c"]; if (mfaCompletedFactors["totp"] == null || mfaCompletedFactors["totp"] < (DateTime.now().millisecondsSinceEpoch - 1000 * 60 * 5)) { // user has not finished TOTP MFA in the last 5 minutes } } else { // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } } } } ``` - In your protected routes, you need to first check if a session exists, and then check that the user has finished all the basic MFA factors for logging in (by checking the value of the `v` boolean in the MFA claim session). If that passes, then perform the step-up auth check. - For checking for step-up auth, get the MFA claim value from the session, and then check if TOTP completed within the last 5 minutes. Only if it did, return true, else return false. --- # Additional Verification - Multi Factor Authentication - Embed the pre-built UI component Source: https://supertokens.com/docs/additional-verification/mfa/embed-the-prebuilt-ui ## Overview ## Before you start These instructions only apply to interfaces that use the pre-built UI components. If you are using a custom UI, the embed instructions depend on your implementation details. The tutorial configures `TOTP` as a secondary factor, but the same set of steps are applicable for other secondary factor types. --- ## Render the TOTP Widget in a page The following example shows the scenario where you have a dedicated route, such as `/totp`, for rendering the TOTP Widget. Upon a successful login, the user will be automatically redirected to the return value of `getRedirectionURL` (defaulting to `/`). ```tsx // highlight-start // highlight-end // @ts-ignore // @ts-ignore SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ TOTP.init({ totpMFAScreen: { disableDefaultUI: true, } }), MultiFactorAuth.init({ // highlight-start getRedirectionURL: async (context) => { if (context.action === "GO_TO_FACTOR") { if (context.factorId === "totp") { return "/totp" } } } // highlight-end }) // ... ], }); function TOTPPage() { const navigate = useNavigate(); return (
// highlight-next-line
); } ```
```tsx // highlight-start // highlight-end // @ts-ignore // @ts-ignore SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ TOTP.init({ totpMFAScreen: { disableDefaultUI: true, }, }), MultiFactorAuth.init({ // highlight-start getRedirectionURL: async (context) => { if (context.action === "GO_TO_FACTOR") { if (context.factorId === "totp") { return "/totp" } } } // highlight-end }) // ... ], }); function TOTPPage() { const history = useHistory(); return (
// highlight-next-line
); } ```
```tsx // highlight-start // highlight-end // @ts-ignore // @ts-ignore SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ TOTP.init({ totpMFAScreen: { disableDefaultUI: true, }, }), MultiFactorAuth.init({ getRedirectionURL: async (context) => { if (context.action === "GO_TO_FACTOR") { if (context.factorId === "totp") { return "/totp" } } } }) // ... ], }); function TOTPPage() { return (
// highlight-next-line
) } ```
In the above code snippet, we: 1. Disabled the default TOTP UI by setting `disableDefaultUI` to `true` inside the TOTP recipe config. 2. Overrode the `getRedirectionURL` function inside the MFA recipe config to redirect to `/totp` whenever we want to show the TOTP factor. Feel free to customize the redirection URLs as needed.
:::caution Not applicable to non-react apps. Please build your own custom UI instead. :::
--- ## Render the TOTP Widget in a popup The following example shows the scenario where you embed the TOTP Widget in a popup, and upon successful login, you aim to close the popup. This is especially useful for step up auth. ```tsx // highlight-start // highlight-end SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ TOTP.init(/* ... */), MultiFactorAuth.init(/* ... */) // ... ], }); function TOTPPopup() { let sessionContext = Session.useSessionContext(); const navigate = useNavigate(); const [isModalOpen, setIsModalOpen] = useState(false); const openModal = () => setIsModalOpen(true); const closeModal = () => setIsModalOpen(false); if (sessionContext.loading) { return null; } return (
{

You are logged In!

UserId: {sessionContext.userId}

} {/* highlight-next-line */}
); } ```
```tsx // highlight-start // highlight-end SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ TOTP.init(/* ... */), MultiFactorAuth.init(/* ... */) // ... ], }); function TOTPPopup() { let sessionContext = Session.useSessionContext(); const history = useHistory(); const [isModalOpen, setIsModalOpen] = useState(false); const openModal = () => setIsModalOpen(true); const closeModal = () => setIsModalOpen(false); if (sessionContext.loading) { return null; } return (
{

You are logged In!

UserId: {sessionContext.userId}

} {/* highlight-next-line */}
); } ```
```tsx // highlight-start // highlight-end SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ TOTP.init(/* ... */), MultiFactorAuth.init(/* ... */) // ... ], }); function TOTPPopup() { let sessionContext = Session.useSessionContext(); const [isModalOpen, setIsModalOpen] = useState(false); const openModal = () => setIsModalOpen(true); const closeModal = () => setIsModalOpen(false); if (sessionContext.loading) { return null; } return (
{

You are logged In!

UserId: {sessionContext.userId}

} {/* highlight-next-line */}
); } ```
:::caution Not applicable to non-react apps. Please build your own custom UI instead. :::
# Additional Verification - Multi Factor Authentication - Protect frontend and backend routes Source: https://supertokens.com/docs/additional-verification/mfa/protect-routes ## Overview This page shows you how to protect your frontend and backend routes to make them accessible only when the user has finished all the MFA challenges configured for them. In both the backend and the frontend, routes are protected based on value of the MFA claim, in the session's access token payload. ## Before you start One thing to note here is that, with **OAuth2 Access Tokens**, you don't need to check the MFA claims. You will get the token once the MFA flow is done. --- ## Protect API routes When you call `MultiFactorAuth.init` in the `supertokens.init` on the backend, SuperTokens **automatically adds a session claim validator globally**. This validator checks that the value of `v` in the [MFA claim](./important-concepts#factors) is `true` before allowing the request to proceed. If the value of `v` is `false`, the validator will send a 403 error to the frontend. :::important This validator is added globally, which means that every time you use `Verify Session` or `Get Session` from the backend SDKs, this check will happen. This means that you don't need to add any extra code on a per API level to enforce MFA. ::: ### Exclude routes from the default check To exclude the default validator check in a certain backend route, you have to update `Verify Session` call. ```tsx let app = express(); app.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), async (req: SessionRequest, res) => { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators } ); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), }, ], }, handler: async (req: SessionRequest, res) => { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators } }) ``` ```tsx let fastify = Fastify(); fastify.post("/update-blog", { preHandler: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), }, async (req: SessionRequest, res) => { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators }); ``` ```tsx async function updateBlog(awsEvent: SessionEvent) { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators }; exports.handler = verifySession(updateBlog, { overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }); ``` ```tsx let router = new KoaRouter(); router.post("/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), async (ctx: SessionContext, next) => { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators }); ``` ```tsx class Example { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/update-blog") @intercept(verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, })) @response(200) async handler() { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators } } ``` ```tsx // highlight-start export default async function example(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, })(req, res, next); }, req, res ) // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators return NextResponse.json({}) }, { // highlight-start overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, // highlight-end }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { @Post('example') @UseGuards(new AuthGuard({ overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, })) async postExample(@Session() session: SessionContainer): Promise { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators return true; } } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim from supertokens_python.recipe.session import SessionContainer from fastapi import Depends @app.post('/like_comment') # type: ignore async def like_comment(session: SessionContainer = Depends( verify_session( # highlight-start # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key] # highlight-end ) )): # All validator checks have passed and the user has a verified email address pass ``` ```python from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim @app.route('/update-jwt', methods=['POST']) # type: ignore @verify_session( # highlight-start # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key] # highlight-end ) def like_comment(): # All validator checks have passed and the user has a verified email address pass ``` ```python from supertokens_python.recipe.session.framework.django.asyncio import verify_session from django.http import HttpRequest from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim @verify_session( # highlight-start # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key] # highlight-end ) async def like_comment(request: HttpRequest): # All validator checks have passed and the user has a verified email address pass ``` The same modification can be done for `getSession` as well. ### Check MFA claim manually To account for a more complex logic when you check the MFA claim (other than checking if `v` is `true`), look over the next code snippet. ```tsx let app = express(); app.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), async (req: SessionRequest, res) => { let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }] }) } } ); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), }, ], }, handler: async (req: SessionRequest, res) => { let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }] }) } } }) ``` ```tsx let fastify = Fastify(); fastify.post("/update-blog", { preHandler: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), }, async (req: SessionRequest, res) => { let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }] }) } }); ``` ```tsx async function updateBlog(awsEvent: SessionEvent) { let mfaClaimValue = await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await awsEvent.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }] }) } }; exports.handler = verifySession(updateBlog, { overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }); ``` ```tsx let router = new KoaRouter(); router.post("/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), async (ctx: SessionContext, next) => { let mfaClaimValue = await ctx.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await ctx.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await ctx.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }] }) } }); ``` ```tsx class Example { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/update-blog") @intercept(verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, })) @response(200) async handler() { let mfaClaimValue = await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await (this.ctx as any).session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }] }) } } } ``` ```tsx // highlight-start export default async function example(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, })(req, res, next); }, req, res ) let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: await superTokensNextWrapper( async (next) => { throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }] }) }, req, res ) } } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } let mfaClaimValue = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: const error = new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }] }) return NextResponse.json(error, { status: 403 }); } return NextResponse.json({}) }, { // highlight-start overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, // highlight-end }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { @Post('example') @UseGuards(new AuthGuard({ overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, })) async postExample(@Session() session: SessionContainer): Promise { let mfaClaimValue = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [{ id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }] }) } return true; } } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from fastapi import Depends from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( MultiFactorAuthClaim, ) from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( ClaimValidationError, raise_invalid_claims_exception, ) from supertokens_python.recipe.session.framework.fastapi import verify_session @app.post("/update-blog") # type: ignore async def update_blog_api(session: SessionContainer = Depends(verify_session())): # highlight-start mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim) if mfa_claim_value is None: # This means that there is no MFA claim information in the session. # This can happen if the session was created prior to enabling the MFA recipe on the backend. # So here, we add the value of the MFA claim to the session: await session.fetch_and_set_claim(MultiFactorAuthClaim) mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim) assert mfa_claim_value is not None completed_factors = mfa_claim_value.c if "totp" not in completed_factors: # The user has not finished TOTP. We throw a claim validation error: raise_invalid_claims_exception( "User has not finished TOTP", [ ClaimValidationError( MultiFactorAuthClaim.key, { "message": "Factor validation failed: totp not completed", "factorId": "totp", }, ) ], ) # If we reach here, it means the user has completed TOTP # highlight-end ``` ```python from flask import Flask, g from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( MultiFactorAuthClaim, ) from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( ClaimValidationError, raise_invalid_claims_exception, ) from supertokens_python.recipe.session.framework.flask import verify_session app = Flask(__name__) @app.route('/update-blog', methods=['POST']) # type: ignore @verify_session() def check_mfa_api(): session: SessionContainer = g.supertokens # type: ignore # highlight-start mfa_claim_value = session.sync_get_claim_value(MultiFactorAuthClaim) if mfa_claim_value is None: # This means that there is no MFA claim information in the session. # This can happen if the session was created prior to enabling the MFA recipe on the backend. # So here, we add the value of the MFA claim to the session: session.sync_fetch_and_set_claim(MultiFactorAuthClaim) mfa_claim_value = session.sync_get_claim_value(MultiFactorAuthClaim) assert mfa_claim_value is not None completed_factors = mfa_claim_value.c if "totp" not in completed_factors: # The user has not finished TOTP. We throw a claim validation error: raise_invalid_claims_exception("User has not finished TOTP", [ ClaimValidationError(MultiFactorAuthClaim.key, { "message": "Factor validation failed: totp not completed", "factorId": "totp", }) ]) # If we reach here, it means the user has completed TOTP # highlight-end ``` ```python from typing import cast from django.http import HttpRequest from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( MultiFactorAuthClaim, ) from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( ClaimValidationError, raise_invalid_claims_exception, ) from supertokens_python.recipe.session.framework.django.asyncio import verify_session @verify_session() async def get_user_info_api(request: HttpRequest): session: SessionContainer = cast(SessionContainer, request.supertokens) # type: ignore # highlight-start mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim) if mfa_claim_value is None: # This means that there is no MFA claim information in the session. # This can happen if the session was created prior to enabling the MFA recipe on the backend. # So here, we add the value of the MFA claim to the session: await session.fetch_and_set_claim(MultiFactorAuthClaim) mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim) assert mfa_claim_value is not None completed_factors = mfa_claim_value.c if "totp" not in completed_factors: # The user has not finished TOTP. We throw a claim validation error: raise_invalid_claims_exception( "User has not finished TOTP", [ ClaimValidationError( MultiFactorAuthClaim.key, { "message": "Factor validation failed: totp not completed", "factorId": "totp", }, ) ], ) # If we reach here, it means the user has completed TOTP # highlight-end ``` - In the code snippet above, we remove the default validator that was added to the global validators (which checks if the `v` value in the claim is true or not). You don't need to do this, but in the code snippet above, we show it anyway. - Then in the API logic, we manually fetch the claim value, and then check if TOTP has been completed or not. If it hasn't, we send back a 403 error to the frontend. You can use a similar approach as shown above to do any kind of check. :::info important If you are doing JWT verification manually, then post verification, you should check the payload of the JWT and make sure that the `v` value in the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) is `true`. This would be equivalent to doing a check as our default claim validator mentioned above. Make sure to also do other checks on the JWT payload. For example, if you require all users to have finished email verification, then we need to check for that claim as well in the JWT. ::: --- ## Protect frontend routes When you call `MultiFactorAuth.init` in the `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you use the `SessionAuth` component. This validator checks if the `v` value in the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) is `true` or not. If it is not, then the user will be redirected to the MFA auth screen. ### Other forms of authorization If you do not want to run our default validator on a specific route, you can modify the use of `SessionAuth` in the following way: ```tsx const VerifiedRoute = (props: React.PropsWithChildren) => { return ( { return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.id); }}> {props.children} ); } function InvalidClaimHandler(props: React.PropsWithChildren) { const claimValue = useClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (claimValue.loading) { return null; } if (claimValue.value === undefined || !("totp" in claimValue.value.c)) { return
You do not have access to this page because you have not completed TOTP. Please click here to finish to proceed.
} // the user has finished TOTP, so we can render the children return
{props.children}
; } ``` - In the snippet above, we remove the default claim validator that is added to `SessionAuth`, and add out own logic that reads from the session's payload. - Finally, we check if the user has completed TOTP or not. If not, we show a message to the user, and ask them to complete TOTP. Of course, if this is all you want to do, then the default validator already does that. But the above has the boilerplate for how you can do more complex checks.
By default, when you do `MultiFactorAuth.init` in `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you call the `Session.validateClaims` function. This validator checks if the `v` value in the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) is `true` or not. ```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims(); if (validationErrors.length === 0) { // user has finished all MFA factors. return true; } else { for (const err of validationErrors) { if (err.id === MultiFactorAuthClaim.id) { // user has not finished MFA factors. let mfaClaimValue = await Session.getClaimValue({ claim: MultiFactorAuthClaim }); if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { // the user has not finished totp return false; } } } } } // a session does not exist, or email is not verified return false } ``` In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it will be reflected in the `validationErrors` variable. The `MultiFactorAuthClaim` validator will be automatically checked by this function since you have initialized the MFA recipe. In case the claim fails, you can get the claim value and check which factor is not completed. In the above code, we check that if it's the TOTP factor that is missing when the claim fails and return `false` from this function. However, it's really up to you for what you want to do next. For example, you could redirect the user to the TOTP factor screen.
By default, when you do `MultiFactorAuth.init` in `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you call the `Session.validateClaims` function. This validator checks if the `v` value in the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) is `true` or not. ```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims(); if (validationErrors.length === 0) { // user has finished all MFA factors. return true; } else { for (const err of validationErrors) { if (err.id === MultiFactorAuthClaim.id) { // user has not finished MFA factors. let mfaClaimValue = await Session.getClaimValue({ claim: MultiFactorAuthClaim }); if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { // the user has not finished totp return false; } } } } } // a session does not exist, or email is not verified return false } ``` ```tsx async function shouldLoadRoute(): Promise { if (await supertokensSession.doesSessionExist()) { let validationErrors = await supertokensSession.validateClaims(); if (validationErrors.length === 0) { // user has finished all MFA factors. return true; } else { for (const err of validationErrors) { if (err.id === supertokensMultiFactorAuth.MultiFactorAuthClaim.id) { // user has not finished MFA factors. let mfaClaimValue = await supertokensSession.getClaimValue({ claim: supertokensMultiFactorAuth.MultiFactorAuthClaim }); if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { // the user has not finished totp return false; } } } } } // a session does not exist, or email is not verified return false } ``` In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it will be reflected in the `validationErrors` variable. The `MultiFactorAuthClaim` validator will be automatically checked by this function since you have initialized the MFA recipe. In case the claim fails, you can get the claim value and check which factor is not completed. In the above code, we check that if it's the TOTP factor that is missing when the claim fails and return `false` from this function. However, it's really up to you for what you want to do next. For example, you could redirect the user to the TOTP factor screen. In your app, you can check the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) values to know which factors have been completed by the user and take action based on that. ```tsx async function checkIfMFAIsCompleted() { if (await SuperTokens.doesSessionExist()) { // highlight-start let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; if (isMFACompleted) { // All required factors for MFA have been completed } else { // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } // highlight-end } } ``` ```kotlin val isMFACompleted: Boolean = (accessTokenPayload.get("st-mfa") as JSONObject).get("v") as Boolean if (isMFACompleted) { // All required factors for MFA have been completed } else { // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } } } ``` ```swift if let mfaObject: [String: Any] = accessTokenPayload["st-mfa"] as? [String: Any] { // Determine if MFA has been completed if let isMFACompleted: Bool = mfaObject["v"] as? Bool { if isMFACompleted { // All required factors for MFA have been completed } else { // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } } } } } } ``` ```dart Future checkIfMFAIsCompleted() async { var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); if (accessTokenPayload.containsKey("st-mfa")) { Map mfaObject = accessTokenPayload["st-mfa"]; if (mfaObject.containsKey("v")) { bool isMFACompleted = mfaObject["v"]; if (isMFACompleted) { // All required factors for MFA have been completed } else { // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } } } } ``` If the MFA claim value is missing in the access token payload, then it means that the session was created before you enabled MFA on the backend. In this case, you can call the [MFA Info](./frontend-setup#mfa-info-endpoint) endpoint which will add the MFA claim to the session and check again. --- # Additional Verification - Multi Factor Authentication - Hooks and overrides Source: https://supertokens.com/docs/additional-verification/mfa/hooks-and-overrides **SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. The following sections describe how you can modify adjust the `mfa` recipe to your needs. Explore the [references pages](/docs/references) for a more in depth guide on hooks and overrides. ## Frontend event hooks The pre-built UI emits a few events that you can listen to on the frontend. As an example, you can use these for analytics: ```tsx SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", onHandleEvent: (context) => { if (context.action === "PASSWORDLESS_CODE_SENT") { // this event is fired when the user has successfully sent out an OTP email / SMS } else if (context.action === "PASSWORDLESS_RESTART_FLOW") { // This event is fired when the user's OTP has expired, or // they have reached the max limit of number of failed OTP attempts. } else if (context.action === "SUCCESS" && !context.createdNewSession) { // this event is fired when successfully completing the OTP email / SMS challenge // and if it's not used in first factor (cause we do !context.createdNewSession) } } }), TOTP.init({ onHandleEvent: (context) => { if (context.action === "TOTP_DEVICE_CREATED") { // this event is fired during factor setup, when the user has successfully created the TOTP device. They still have to verify it by entering the TOTP. } else if (context.action === "TOTP_DEVICE_VERIFIED") { // this event is fired during factor setup, when the user has successfully verified the TOTP device } else if (context.action === "TOTP_CODE_VERIFIED") { // this event is fired when the user has successfully verified the TOTP code // marking the TOTP factor as completed } } }), MultiFactorAuth.init({ firstFactors: [/*...*/], onHandleEvent: (context) => { if (context.action === "FACTOR_CHOOSEN") { let chosenFactorId = context.factorId; // this event is fired when the user is shown the screen for // picking one factor out of a choice of multiple factors } } }) ] }) ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ supertokensUIPasswordless.init({ contactMethod: "EMAIL_OR_PHONE", onHandleEvent: (context) => { if (context.action === "PASSWORDLESS_CODE_SENT") { // this event is fired when the user has successfully sent out an OTP email / SMS } else if (context.action === "PASSWORDLESS_RESTART_FLOW") { // This event is fired when the user's OTP has expired, or // they have reached the max limit of number of failed OTP attempts. } else if (context.action === "SUCCESS" && !context.createdNewSession) { // this event is fired when successfully completing the OTP email / SMS challenge // and if it's not used in first factor (cause we do !context.createdNewSession) } } }), supertokensUITOTP.init({ onHandleEvent: (context) => { if (context.action === "TOTP_DEVICE_CREATED") { // this event is fired during factor setup, when the user has successfully created the TOTP device. They still have to verify it by entering the TOTP. } else if (context.action === "TOTP_DEVICE_VERIFIED") { // this event is fired during factor setup, when the user has successfully verified the TOTP device } else if (context.action === "TOTP_CODE_VERIFIED") { // this event is fired when the user has successfully verified the TOTP code // marking the TOTP factor as completed } } }), supertokensUIMultiFactorAuth.init({ firstFactors: [/*...*/], onHandleEvent: (context) => { if (context.action === "FACTOR_CHOOSEN") { let chosenFactorId = context.factorId; // this event is fired when the user is shown the screen for // picking one factor out of a choice of multiple factors } } }) ] }) ``` ## Backend overrides It's a common use case to want to override the default behavior of SuperTokens after a user signs up or signs in. For example, you may want to change your database state whenever someone signs up. You can do this by overriding the sign up / sign in recipe functions in the backend SDK: - [Passwordless recipe](/docs/authentication/passwordless/hooks-and-overrides) - [EmailPassword recipe](/docs/authentication/email-password/hooks-and-overrides) - [ThirdParty recipe](/docs/authentication/social/hooks-and-overrides) Since the sign up / sign in APIs share functionality for first factor and second factor login, your override applies to both first and second factor login. If you want to have different behavior for first and second factor login, you can use the `input` argument to the function to determine if the user is doing first or second factor login. The `input` argument contains the `session` object using which you can determine if the user is doing first or second factor login. If the `session` property is `undefined`, it means it's a first factor login, else it's a second factor login. In the links above, the code snippets check for `input.session === undefined` to determine if it's a first factor login. # Additional Verification - Multi Factor Authentication - Migration - Migration from legacy MFA to new MFA method Source: https://supertokens.com/docs/additional-verification/mfa/migration/legacy-to-new This page only applies if you have used our [legacy MFA method](/docs/additional-verification/mfa/legacy-mfa/how-it-works) and want to migrate to the new MFA method. From a code point of view, [here is the diff](https://github.com/supertokens/supertokens-auth-react/commit/f96e11a527ccd9df71c25798c316dd3de770dde1) of all changes required: - We no longer need the custom `secondFactoeClaim` since the MFA recipe adds its own MFA claim. - On the backend, we initialise the MFA recipe: - In the example app, we use email password and social login as the first factor, so the we set `firstFactors` variable to to `[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY]`. - We also set the required second factor to return OTP phone, but you can set it to OTP email as well. - We override the `resyncSessionAndFetchMFAInfoPUT` API to sync old session claim with the new one. This API is called from the frontend during sign in, and also whenever you use `` (for the pre-built UI) or call `await Session.validateClaims()` for your custom UI. - We remove the custom overrides for Passwordless.init and for Session.init since those are now handled by the new MFA recipe. - We initialise the AccountLinking recipe with the config that enables account linking for first and second factors. - A side effect of using our multi factor auth is that if you have enabled the email verification recipe, we will ask users to complete email verification before completing any secondary factors. - On the frontend for pre-built UI: - You will no longer need to use the custom SecondFactorClaim, and also no longer need to create a SecondFactor component (since we provide a pre-built one). - We initialise the MultiFactorAuth recipe, and remove the Session.init override from before as well. - On the frontend for custom UI: - You no longer will need to use the custom SecondFactorClaim and instead, you can see our guide for email / SMS OTP in the above sections. ## Understanding the override code for `resyncSessionAndFetchMFAInfoPUT` We start by getting the user object from of the session's user ID along with the user's metadata. If the user object does not already contain a passwordless login method, it means we have not linked the passwordless user yet. If also, the metadata has the `passwordlessUserId` prop in it, it means that this user had previously setup email / SMS OTP as a second factor. In this case, we now attempt to link the passwordless user to the session user. For linking, we need to first ensure that the session user is a primary user, and if not, we call the `createPrimaryUser` function from the account linking recipe. After that, we call the `linkAccounts` function with the session user id and the passwordless user ID (pulled from the metadata). We are now in a state where the session user has two login methods: - The first factor login method - The passwordless login method for 2FA Finally, we update the session claim value to contain the MFA claim and also mark the `OTP_PHONE` factor as completed (you can use `OTP_EMAIL`). This will prevent users from being asked to complete the second factor again if they have already done so in the current session. # Additional Verification - Multi Factor Authentication - Migration - Migration from an older SuperTokens SDK to a newer one Source: https://supertokens.com/docs/additional-verification/mfa/migration/old-sdk-to-new 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.md 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 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, then you may want to remove the global MFA validator on the backend and make it run only when you know that the request is coming from an updated client: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ // other recipes.. Session.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, // highlight-start 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 modificationts 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 => { // We only want to run the validation check // if we know that the client is not an older one. // In this example, we do this based on the header // in the API request, but the logic here can be // anything that you like. let request = getRequestFromUserContext(userContext); if (request !== undefined) { let isOlderClient = request.getHeaderValue("clilent-version") !== "2.0"; if (isOlderClient) { // we return true early for older clients. 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]; } // highlight-end } } } }) ] }) ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python 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, ) from supertokens_python import get_request_from_user_context 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: # Check if the client is an older one based on the header request = get_request_from_user_context(user_context) if request is not None: is_older_client = request.get_header("client-version") != "2.0" if is_older_client: # Return true early for older clients 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="...", # type: ignore recipe_list=[ session.init(override=InputOverrideConfig(functions=override_functions)) ], ) ``` The code above will still result in the MFA claim being added to all sessions, with the `v` boolean in it being `false`, however, for older clients, we will not run the validator that checks if the `v` value is true on the backend since those clients have no way to show the MFA UI without updating the app. :::caution You want to add the above exception only for a certain amount of time until you force all users to update their mobile app. This is because the exception method can be used as a way for malicious users to bypass MFA by spoofing that they are using an older client. ::: # Additional Verification - Multi Factor Authentication - Legacy method - Legacy vs New method Source: https://supertokens.com/docs/additional-verification/mfa/legacy-mfa/legacy-vs-new The legacy method for MFA requires you to customise the auth recipes we have to add MFA on top of it. Since it does not use the MFA recipe we have, it's free. The limitations of using the legacy method are: - It does not support TOTP, but only email / SMS OTP - When the end user completes the email / SMS OTP factor, there will be a separate user that's created for that login method which is not linked to the first factor login method for that user. This means that you will see two SuperTokens users for every single end use that goes through the MFA flow. - It requires multiple customisations on top of the basic auth setup, which adds scope for error. When should you use the legacy method? - If you are not using our Node backend SDK, then you have to use this, since we do not yet support the MFA recipe for non Node SDKs. - If you are price sensitive and do not want to pay for the MFA recipe then you can use this method. That being said, if you are using our managed service, then depending on your end user's login pattern, the number of MAUs you will be charged for is double (assuming all users complete 2FA each month) than the actual MAU count. This in turn is more expensive than using the MFA recipe. # Additional Verification - Multi Factor Authentication - Legacy method - How it works Source: https://supertokens.com/docs/additional-verification/mfa/legacy-mfa/how-it-works :::info Caution This is the legacy method of implementing MFA. It has multiple [disadvantages](./legacy-vs-new) compared to using our MFA recipe. ::: You need to start by choosing your first factor auth. This can be any of the [auth recipes](https://supertokens.com/docs/guides) we support. A common choice is to combine the `thirdparty` and `emailpassword` recipes, which allows users to sign in with social or email / password login. For the second factor, whilst you can choose any of our auth recipes as well, the most common choice is the [Passwordless recipe](https://supertokens.com/docs/passwordless/introduction), using which, you can send SMS or email OTP (or magic links) to the user. You will also need to use our Session recipe which can be used to store information about which factors have been completed by the user for the current session. This will be integral to our implementation. As a high level flow, we will customise these recipes in the following way: - After the first factor is completed, we will override the `createNewSession` function in the Session recipe to store the fact that only the first factor is complete. We do this by adding the `SecondFactorClaim` to the session which will default to false. - After the second factor is completed, we will update the session and set `SecondFactorClaim` to true. - To make sure that application APIs are only accessible post 2FA is completed, we will add a `SecondFactorClaim` validator to the global validators by overriding `getGlobalClaimValidators` in the Session recipe. This will check if the second factor has been completed whenever `verifySession` or `getSession` is called. - Similarly, to protect frontend routes, we will add the `SecondFactorClaim` validator to the global validators ensuring that all components wrapped with the `SessionAuth` component will check that 2FA is completed. If not, we can then reroute the user to the second factor screen. - We also need to use the `UserMetadata` recipe to store information about the second factor authentication's identification. In the example app, we use phone number SMS OTP as the second factor, therefore we store the user's phone number using the `UserMetadata` recipe, and only send OTPs to that number during sign in. The phone number itself is obtained during the sign up flow. :::info Important In the subsequent sections, we will see how to implement 2fa with the first factor being social and email / password login, and the second factor being phone SMS OTP. If you require a different set of factors or behaviour, you can take inspiration from this guide. ::: # Additional Verification - Multi Factor Authentication - Legacy method - Backend Setup - Setting up the 1st factor Source: https://supertokens.com/docs/additional-verification/mfa/legacy-mfa/backend-setup/first-factor ## 1. Initialisation Start by following the recipe guide for the first factor. In this guide, we will take the example of `thirdparty` and `emailpassword` recipes as being the first factor. After following the [backend quick setup section](/docs/quickstart/backend-setup) (or any of the framework specific integration guides), you should have all the auth APIs exposed to the frontend via the SuperTokens middleware. The `supertokens.init` code on the server would look like this: ```tsx supertokens.init({ framework: "express", supertokens: { connectionURI: "", apiKey: "^{coreInfo.key}", }, appInfo: { // learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, recipeList: [ ThirdParty.init({ //... }), EmailPassword.init({ //... }), Session.init(), // initializes session features UserMetadata.init() // initializes the user metadata feature ] }); ``` ```tsx supertokens.init({ framework: "hapi", supertokens: { connectionURI: "", apiKey: "^{coreInfo.key}", }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, recipeList: [ ThirdParty.init({ //... }), EmailPassword.init({ //... }), Session.init(), // initializes session features UserMetadata.init() // initializes the user metadata feature ] }); ``` ```tsx supertokens.init({ framework: "fastify", supertokens: { connectionURI: "", apiKey: "^{coreInfo.key}", }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, recipeList: [ ThirdParty.init({ //... }), EmailPassword.init({ //... }), Session.init(), // initializes session features UserMetadata.init() // initializes the user metadata feature ] }); ``` ```tsx supertokens.init({ framework: "koa", supertokens: { connectionURI: "", apiKey: "^{coreInfo.key}", }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, recipeList: [ ThirdParty.init({ //... }), EmailPassword.init({ //... }), Session.init(), // initializes session features UserMetadata.init() // initializes the user metadata feature ] }); ``` ```tsx supertokens.init({ framework: "loopback", supertokens: { connectionURI: "", apiKey: "^{coreInfo.key}", }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, recipeList: [ ThirdParty.init({ //... }), EmailPassword.init({ //... }), Session.init(), // initializes session features UserMetadata.init() // initializes the user metadata feature ] }); ``` // // // :::important // Please refer the **serverless deployment** section in the ThirdParty + EmailPassword recipe guide // ::: // // // // // :::important // Please refer the **NextJS** section in the ThirdParty + EmailPassword recipe guide // ::: // // // // // :::important // Please refer the **NestJS** section in the ThirdParty + EmailPassword recipe guide // ::: // // ```go showAppTypeSelect /* This will be used to modify the session's access token payload to add {"2fa-completed": false} into it. */ // highlight-start export const SecondFactorClaim = new BooleanClaim({ fetchValue: () => false, key: "2fa-completed", }); // highlight-end Session.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, /* This function is called after signing in or signing up via the first factor */ createNewSession: async function (input) { return originalImplementation.createNewSession({ ...input, accessTokenPayload: { ...input.accessTokenPayload, // highlight-next-line ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)), }, }); }, }; }, }, }) ``` ```go # Additional Verification - Multi Factor Authentication - Legacy method - Backend Setup - Setting up the 2nd factor Source: https://supertokens.com/docs/additional-verification/mfa/legacy-mfa/backend-setup/second-factor ## 1. Initialisation We use the [Passwordless recipe](https://supertokens.com/docs/passwordless/introduction) with SMS OTP as the second factor. You can follow the recipe's [backend quick setup guide](https://supertokens.com/docs/passwordless/quick-setup/backend) to configure a different method as well (for example with email magic links). The `Passwordless.init` function should look something like this: ```tsx supertokens.init({ framework: "express", supertokens: { connectionURI: "", apiKey: "^{coreInfo.key}", }, appInfo: { // learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, recipeList: [ // highlight-start Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE" }), // highlight-end ThirdParty.init({ //... }), EmailPassword.init({ //... }), Session.init({/*Override from previous step*/}) ] }); ``` ```tsx supertokens.init({ framework: "hapi", supertokens: { connectionURI: "", apiKey: "^{coreInfo.key}", }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, recipeList: [ // highlight-start Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE" }), // highlight-end ThirdParty.init({ //... }), EmailPassword.init({ //... }), Session.init({ /*Override from previous step*/ }) ] }); ``` ```tsx supertokens.init({ framework: "fastify", supertokens: { connectionURI: "", apiKey: "^{coreInfo.key}", }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, recipeList: [ // highlight-start Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE" }), // highlight-end ThirdParty.init({ //... }), EmailPassword.init({ //... }), Session.init({/*Override from previous step*/}) ] }); ``` ```tsx supertokens.init({ framework: "koa", supertokens: { connectionURI: "", apiKey: "^{coreInfo.key}", }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, recipeList: [ // highlight-start Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE" }), // highlight-end ThirdParty.init({ //... }), EmailPassword.init({ //... }), Session.init({/*Override from previous step*/}) ] }); ``` ```tsx supertokens.init({ framework: "loopback", supertokens: { connectionURI: "", apiKey: "^{coreInfo.key}", }, appInfo: { // learn more about this on https://supertokens.com/docs/session/appinfo appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, recipeList: [ // highlight-start Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE" }), // highlight-end ThirdParty.init({ //... }), EmailPassword.init({ //... }), Session.init({/*Override from previous step*/}) ] }); ``` :::important Please refer the **serverless deployment** section in the Passwordless recipe guide ::: :::important Please refer the **NextJS** section in the Passwordless recipe guide ::: :::important Please refer the **NestJS** section in the Passwordless recipe guide ::: ```go showAppTypeSelect }), }, }) if err != nil { panic(err.Error()) } } ``` ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, emailpassword, session, passwordless from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="", website_base_path="" ), supertokens_config=SupertokensConfig( connection_uri="", api_key="^{coreInfo.key}" ), framework='fastapi', recipe_list=[ session.init(), # contains the override from the previous step thirdparty.init( # ... ), emailpassword.init( # ... ), # highlight-start passwordless.init( flow_type="USER_INPUT_CODE", contact_config=ContactPhoneOnlyConfig() ) # highlight-end ], mode='asgi' # use wsgi if you are running using gunicorn ) ``` ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, emailpassword, session, passwordless from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="", website_base_path="" ), supertokens_config=SupertokensConfig( connection_uri="", api_key="^{coreInfo.key}" ), framework='flask', recipe_list=[ session.init(), # contains the override from the previous step thirdparty.init( # ... ), emailpassword.init( # ... ), # highlight-start passwordless.init( flow_type="USER_INPUT_CODE", contact_config=ContactPhoneOnlyConfig() ) # highlight-end ] ) ``` ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, emailpassword, session, passwordless from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="", website_base_path="" ), supertokens_config=SupertokensConfig( connection_uri="", api_key="^{coreInfo.key}" ), framework='django', recipe_list=[ session.init(), # contains the override from the previous step thirdparty.init( # ... ), emailpassword.init( # ... ), # highlight-start passwordless.init( flow_type="USER_INPUT_CODE", contact_config=ContactPhoneOnlyConfig() ) # highlight-end ], mode='asgi' # use wsgi if you are running django server in sync mode ) ``` The above exposes all the APIs to the frontend that can be used to create and verify the OTP. ## 2. Saving the user's phone number post second factor auth During sign up, once the user has completed the second factor, we want to save their phone number against their profile. For this, we use the `UserMetadata` recipe. :::important Make sure to add the User Metadata in the recipe list. ::: The passwordless recipe creates a new `userId` for the user against which it saves the phone number. We can associate the passwordless `userId` with the `userId` of the first factor, and this way, we associate a phone number to the user: ```tsx Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE", override: { apis: (oI) => { return { ...oI, // this API is called when the user enters the OTP consumeCodePOST: async function (input) { // - We should already have a session here since this is called after first factor login // - We set the claims to check to be [] here, since this needs to be callable // without the second factor completed let session = await Session.getSession(input.options.req, input.options.res, { overrideGlobalClaimValidators: () => [], }); let resp = await oI.consumeCodePOST!(input); if (resp.status === "OK") { // OTP verification was successful. We can now associate // the passwordless user ID with the thirdpartyemailpassword // user ID, so that later on, we can fetch the phone number. await UserMetadata.updateUserMetadata( session!.getUserId(), // this is the userId of the first factor login { passwordlessUserId: resp.user.id, } ); } return resp; }, }; }, } }) ``` ```go declare const SecondFactorClaim: BooleanClaim; // REMOVE_FROM_OUTPUT Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE", override: { apis: (oI) => { return { ...oI, // this API is called when the user enters the OTP consumeCodePOST: async function (input) { // A session should already exist since this should be called after the first factor is completed. // We set the claims to check to be [] here, since this needs to be callable // without the second factor completed let session = await Session.getSession(input.options.req, input.options.res, { overrideGlobalClaimValidators: () => [], }); // highlight-start // we add the existing session to the user context so that the createNewSession // function doesn't create a new session input.userContext.session = session; // highlight-end let resp = await oI.consumeCodePOST!(input); if (resp.status === "OK") { // highlight-start // OTP verification was successful. // We can now set the SecondFactorClaim in the session to true. // the user has access to API routes and the frontend UI await resp.session.setClaimValue(SecondFactorClaim, true); // highlight-end // We can now associate // the passwordless user ID with the thirdpartyemailpassword // user ID, so that later on, we can fetch the phone number. await UserMetadata.updateUserMetadata( session!.getUserId(), // this is the userId of the first factor login { passwordlessUserId: resp.user.id, } ); } return resp; }, }; }, } }) Session.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, /* This function is called after signing in or signing up via the first factor */ createNewSession: async function (input) { // highlight-start if (input.userContext.session !== undefined) { /** * This is true for the second factor login. * So instead of creating a new session, we return the already existing one. */ return input.userContext.session; } // highlight-end return originalImplementation.createNewSession({ ...input, accessTokenPayload: { ...input.accessTokenPayload, ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)), }, }); }, }; }, }, }) ``` ```go /** * This will be true for the second factor login. * So instead of creating a new session, we return the already existing one. */ return session, nil } // highlight-end if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } accessTokenPayload, err := SecondFactorClaim.Build(userID, tenantId, accessTokenPayload, userContext) if err != nil { return nil, err } return oCreateNewSession(userID, accessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext) } return originalImplementation }, }, }) } ``` ```python from supertokens_python.recipe.passwordless.interfaces import ( APIInterface, APIOptions, ConsumeCodePostOkResult, ) from typing import Union, Dict, Any, Optional from supertokens_python.recipe.session.asyncio import get_session from supertokens_python.recipe.usermetadata.asyncio import update_user_metadata from supertokens_python.recipe.session.interfaces import ( SessionContainer, RecipeInterface, ) from supertokens_python.recipe.session.claims import BooleanClaim from supertokens_python.types import RecipeUserId SecondFactorClaim = BooleanClaim( key="2fa-completed", fetch_value=lambda _, __, ___, ____, _____: False ) def override_passwordless_apis(original_implementation: APIInterface): original_consume_code_post = original_implementation.consume_code_post async def consume_code_post( pre_auth_session_id: str, user_input_code: Union[str, None], device_id: Union[str, None], link_code: Union[str, None], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): # this API is called when the user enters the OTP # A session should already exist since this should be called after the first factor is completed. # We set the claims to check to be [] here, since this needs to be callable # without the second factor completed _session = await get_session( api_options.request, override_global_claim_validators=lambda _, __, ___: [] ) assert _session is not None # we should add the existing session to the user_context # so that the create_new_session function # doesn't create a new session # highlight-next-line user_context["session"] = _session res = await original_consume_code_post( pre_auth_session_id, user_input_code, device_id, link_code, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) if isinstance(res, ConsumeCodePostOkResult): # highlight-start # OTP verification was successful. We can now mark the # session's payload as {"is2faComplete": True} so that # the user has access to API routes and the frontend UI await _session.set_claim_value(SecondFactorClaim, True) # highlight-end # We can now associate # the passwordless user ID with the thirdpartyemailpassword # user ID, so that later on, we can fetch the phone number. await update_user_metadata( _session.get_user_id(), # userId of the first factor login {"passwordlessUserId": res.user.id}, ) return res original_implementation.consume_code_post = consume_code_post return original_implementation def override_session_functions(original_implementation: RecipeInterface): original_create_new_session = original_implementation.create_new_session async def create_new_session( user_id: str, recipe_user_id: RecipeUserId, access_token_payload: Optional[Dict[str, Any]], session_data_in_database: Optional[Dict[str, Any]], disable_anti_csrf: Optional[bool], tenant_id: str, user_context: Dict[str, Any], ): # This function is called after signing in or # signing up via the first factor # highlight-start _session = user_context.get("session") if _session and isinstance(_session, SessionContainer): # This is true for the second factor login. # So instead of creating a new session, we return the already existing one. return _session # highlight-end if access_token_payload is None: access_token_payload = {} access_token_payload = { **access_token_payload, **( await SecondFactorClaim.build( user_id, recipe_user_id, tenant_id, access_token_payload, user_context, ) ), } return await original_create_new_session( user_id, recipe_user_id, access_token_payload, session_data_in_database, disable_anti_csrf, tenant_id, user_context, ) original_implementation.create_new_session = create_new_session return original_implementation ``` ## 4. Validating the phone number By default, the Passwordless API for sending an OTP (`createCodePOST`) sends the OTP to the input phone number, and if we don't modify that, the attack below is be possible: - Alice (user) signs up using a weak password and their phone number. - Mallory (attacker) successfully guesses Alice's password and queries the OTP sending API manually, to inject their phone number for the second factor auth. - OTP is sent to Mallory's phone number and they can pass the second factor challenge. To make it secure, we override the `createCodePOST` API and check that the input phone number is the same as the phone number associated with the user. If it's not the same, we throw an error, and if it is the same, we continue: ```tsx Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE", override: { apis: (oI) => { return { ...oI, /*This API is called to send an OTP*/ createCodePOST: async function (input) { /** * We want to make sure that the OTP being generated is for the * same number that belongs to this user. */ // A session should already exist since this should be called after the first factor is completed. // We set the claims to check to be [] here, since this needs to be callable // without the second factor completed let session = await Session.getSession(input.options.req, input.options.res, { overrideGlobalClaimValidators: () => [], }); // We try and get the phone number associated with this user. It is // defined if this is a sign in attempt, in which case, we check that // it is equal to the input phone number let userMetadata = await UserMetadata.getUserMetadata(session!.getUserId()); let phoneNumber: string | undefined = undefined; if (userMetadata.metadata.passwordlessUserId !== undefined) { // the flow comes here during a login attempt, since we // associate the passwordless userId to the user on sign up let passwordlessUserInfo = await SuperTokens.getUser( userMetadata.metadata.passwordlessUserId as string, input.userContext, ); phoneNumber = passwordlessUserInfo?.phoneNumbers[0]; } if (phoneNumber !== undefined) { // this means we found a phone number associated to this user. // we check if the input phone number is the same as this one. if (!("phoneNumber" in input) || input.phoneNumber !== phoneNumber) { throw new Error("Input phone number is not the same as the one saved for this user"); } } return oI.createCodePOST!(input); }, consumeCodePOST: async function (input) { /*...Modifications from previous step */ let resp = await oI.consumeCodePOST!(input); /*...Modifications from previous step */ return resp; }, }; }, } }) ``` ```go // the flow comes here during a login attempt, since we // associate the passwordless userId to the user on sign up passwordlessUserInfo, err := passwordless.GetUserByID(passwordlessUserId, userContext) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } userPhoneNumber = passwordlessUserInfo.PhoneNumber } if userPhoneNumber != nil { // this means we found a phone number associated to this user. // we will check if the input phone number is the same as this one. if phoneNumber == nil || *phoneNumber != *userPhoneNumber { return plessmodels.CreateCodePOSTResponse{}, errors.New("Input phone number is not the same as the one saved for this user") } } return oCreateCodePOST(email, phoneNumber, tenantId, options, userContext) } // highlight-end *originalImplementation.CreateCodePOST = nCreateCodePOST oConsumeCodePOST := *originalImplementation.ConsumeCodePOST nConsumeCodePost := func(userInput *plessmodels.UserInputCodeWithDeviceID, linkCode *string, preAuthSessionID string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.ConsumeCodePOSTResponse, error) { /*...mofications from previous step */ resp, err := oConsumeCodePOST(userInput, linkCode, preAuthSessionID, tenantId, options, userContext) /*...mofications from previous step */ return resp, err } *originalImplementation.ConsumeCodePOST = nConsumeCodePost return originalImplementation }, }, }) } ``` ```python from supertokens_python.recipe.passwordless.interfaces import APIInterface, APIOptions from typing import Union, Dict, Any, Optional from supertokens_python.recipe.session.asyncio import get_session from supertokens_python.recipe.usermetadata.asyncio import get_user_metadata from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.asyncio import get_user def override_passwordless_apis(original_implementation: APIInterface): original_consume_code_post = original_implementation.consume_code_post original_create_code_post = original_implementation.create_code_post # highlight-start async def create_code_post( email: Union[str, None], phone_number: Union[str, None], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): # This API is called to send an OTP # We want to make sure that the OTP being generated is for the # same number that belongs to this user. # A session should already exist since this should be called after the first factor is completed. # We set the claims to check to be [] here, since this needs to be callable # without the second factor completed _session = await get_session( api_options.request, override_global_claim_validators=lambda _, __, ___: [] ) assert _session is not None # We try to get the phone number associated with this user. It is # defined if this is a sign in attempt, in which case, we check that # it is equal to the input phone number user_metadata = await get_user_metadata(_session.get_user_id()) user_metadata_phone_number: Optional[str] = None if user_metadata.metadata.get("passwordlessUserId"): # the flow comes here during a login attempt, since we # associate the passwordless userId to the user on sign up passwordless_user_info = await get_user( user_metadata.metadata["passwordlessUserId"], user_context ) if passwordless_user_info is not None: user_metadata_phone_number = passwordless_user_info.phone_numbers[0] if user_metadata_phone_number is not None: # this means we found a phone number associated to this user # we will check if the input phone number is the same as this one. if (phone_number is None) or (phone_number != user_metadata_phone_number): raise Exception( "Input phone number is not the same as the one saved for this user" ) return await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) # highlight-end async def consume_code_post( pre_auth_session_id: str, user_input_code: Union[str, None], device_id: Union[str, None], link_code: Union[str, None], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): # ...Modifications from previous step res = await original_consume_code_post( pre_auth_session_id, user_input_code, device_id, link_code, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) # ...Modifications from previous step return res original_implementation.create_code_post = create_code_post original_implementation.consume_code_post = consume_code_post return original_implementation ``` ## 5. Storing the user's phone number in the session When the session is first created (after the first factor is completed), we store the user's phone number in the session (if it exists), so that the frontend can call the `createCodePOST` API (to initiate the second factor challenge) without asking the user for their phone number again. We do this by modifying the `createNewSession` function in the `Session.init` call: ```tsx declare const SecondFactorClaim: BooleanClaim; // REMOVE_FROM_OUTPUT Session.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, /* This function is called after signing in or signing up via the first factor */ createNewSession: async function (input) { if (input.userContext.session !== undefined) { /** * This is true for the second factor login. * So instead of creating a new session, we return the already existing one. */ return input.userContext.session; } // highlight-start // we first get the passwordless userId associated with this user // using the UserMetadata recipe let userMetadata = await UserMetadata.getUserMetadata(input.userId); let phoneNumber: string | undefined = undefined; if (userMetadata.metadata.passwordlessUserId !== undefined) { // We get the phone number associated with the passwordless userId. let passwordlessUserInfo = await SuperTokens.getUser( userMetadata.metadata.passwordlessUserId as string, input.userContext, ); phoneNumber = passwordlessUserInfo?.phoneNumbers[0]; } // highlight-end return originalImplementation.createNewSession({ ...input, accessTokenPayload: { ...input.accessTokenPayload, ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)), // highlight-next-line phoneNumber, }, }); }, }; }, }, }) ``` ```go /** * This will be true for the second factor login. * So instead of creating a new session, we return the already existing one. */ return session, nil } // highlight-start // we first get the passwordless userId associated with this user // using the UserMetadata recipe userMetadata, err := usermetadata.GetUserMetadata(userID, userContext) if err != nil { return nil, err } var userPhoneNumber *string if passwordlessUserId, ok := userMetadata["passwordlessUserId"].(string); ok { passwordlessUserInfo, err := passwordless.GetUserByID(passwordlessUserId, userContext) if err != nil { return nil, err } userPhoneNumber = passwordlessUserInfo.PhoneNumber } // highlight-end if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } accessTokenPayload, err = SecondFactorClaim.Build(userID, tenantId, accessTokenPayload, userContext) if err != nil { return nil, err } // highlight-start if userPhoneNumber != nil { accessTokenPayload["phoneNumber"] = *userPhoneNumber } // highlight-end return oCreateNewSession(userID, accessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext) } return originalImplementation }, }, }) } ``` ```python from typing import Dict, Any, Optional from supertokens_python.recipe.usermetadata.asyncio import get_user_metadata from supertokens_python.asyncio import get_user from supertokens_python.recipe.session.interfaces import ( SessionContainer, RecipeInterface, ) from supertokens_python.recipe.session.claims import BooleanClaim from supertokens_python.types import RecipeUserId SecondFactorClaim = BooleanClaim( key="2fa-completed", fetch_value=lambda _, __, ___, ____, _____: False ) def override_session_functions(original_implementation: RecipeInterface): original_create_new_session = original_implementation.create_new_session async def create_new_session( user_id: str, recipe_user_id: RecipeUserId, access_token_payload: Optional[Dict[str, Any]], session_data_in_database: Optional[Dict[str, Any]], disable_anti_csrf: Optional[bool], tenant_id: str, user_context: Dict[str, Any], ): # This function is called after signing in # or signing up via the first factor _session = user_context.get("session") if _session and isinstance(_session, SessionContainer): # This is true for the second factor login. # So instead of creating a new session, we return the already existing one. return _session if access_token_payload is None: access_token_payload = {} # highlight-start # we first get the passwordless user id associated with this user # using the user_metadata recipe user_metadata = await get_user_metadata(user_id) phone_number: Optional[str] = None if user_metadata.metadata.get("passwordlessUserId") is not None: # We get the phone number associated with the passwordless userId passwordless_user_info = await get_user( user_metadata.metadata["passwordlessUserId"], user_context ) if passwordless_user_info is not None: phone_number = passwordless_user_info.phone_numbers[0] # highlight-end # Insert "is2faComplete" and "phoneNumber" in the access token payload access_token_payload = { **access_token_payload, **( await SecondFactorClaim.build( user_id, recipe_user_id, tenant_id, access_token_payload, user_context, ) ), # highlight-next-line "phoneNumber": phone_number, } return await original_create_new_session( user_id, recipe_user_id, access_token_payload, session_data_in_database, disable_anti_csrf, tenant_id, user_context, ) original_implementation.create_new_session = create_new_session return original_implementation ``` We can then further modify the customisation in step (4) to simply read from the session's payload making it more efficient: ```tsx Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE", override: { apis: (oI) => { return { ...oI, /*This API is called to send an OTP*/ createCodePOST: async function (input) { /** * We want to make sure that the OTP being generated is for the * same number that belongs to this user. */ // A session should already exist since this should be called after the first factor is completed. // We remove claim checking here, since this needs to be callable without the second factor completed let session = await Session.getSession(input.options.req, input.options.res, { overrideGlobalClaimValidators: () => [], }); // highlight-next-line let phoneNumber: string = session!.getAccessTokenPayload().phoneNumber; if (phoneNumber !== undefined) { // this means we found a phone number associated to this user. // we check if the input phone number is the same as this one. if (!("phoneNumber" in input) || input.phoneNumber !== phoneNumber) { throw new Error("Input phone number is not the same as the one saved for this user"); } } return oI.createCodePOST!(input); }, consumeCodePOST: async function (input) { /*...Modifications from previous step */ let resp = await oI.consumeCodePOST!(input); /*...Modifications from previous step */ return resp; }, }; }, } }) ``` ```go userPhoneNumber = &phoneNumber } // highlight-end if userPhoneNumber != nil { // this means we found a phone number associated to this user. // we will check if the input phone number is the same as this one. if phoneNumber == nil || *phoneNumber != *userPhoneNumber { return plessmodels.CreateCodePOSTResponse{}, errors.New("Input phone number is not the same as the one saved for this user") } } return oCreateCodePOST(email, phoneNumber, tenantId, options, userContext) } *originalImplementation.CreateCodePOST = nCreateCodePOST oConsumeCodePOST := *originalImplementation.ConsumeCodePOST nConsumeCodePost := func(userInput *plessmodels.UserInputCodeWithDeviceID, linkCode *string, preAuthSessionID string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.ConsumeCodePOSTResponse, error) { /*...mofications from previous step */ resp, err := oConsumeCodePOST(userInput, linkCode, preAuthSessionID, tenantId, options, userContext) /*...mofications from previous step */ return resp, err } *originalImplementation.ConsumeCodePOST = nConsumeCodePost return originalImplementation }, }, }) } ``` ```python from supertokens_python.recipe.passwordless.interfaces import APIInterface, APIOptions from typing import Union, Dict, Any, Optional from supertokens_python.recipe.session.asyncio import get_session from supertokens_python.recipe.session.interfaces import SessionContainer def override_passwordless_apis(original_implementation: APIInterface): original_create_code_post = original_implementation.create_code_post async def create_code_post( email: Union[str, None], phone_number: Union[str, None], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): # This API is called to send an OTP # We want to make sure that the OTP being generated is for the # same number that belongs to this user. # A session should already exist since this should be called after the first factor is completed. # We set the claims to check to be [] here, since this needs to be callable # without the second factor completed _session = await get_session( api_options.request, override_global_claim_validators=lambda _, __, ___: [] ) assert _session is not None # highlight-next-line payload_phone_number = _session.get_access_token_payload().get("phoneNumber") if payload_phone_number is not None: # this means we found a phone number associated to this user # we will check if the input phone number is the same as this one. if (phone_number is None) or (phone_number != payload_phone_number): raise Exception( "Input phone number is not the same as the one saved for this user" ) return await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) original_implementation.create_code_post = create_code_post ``` # Additional Verification - Multi Factor Authentication - Legacy method - Backend Setup - Protecting API routes Source: https://supertokens.com/docs/additional-verification/mfa/legacy-mfa/backend-setup/protecting-api In the previous steps, we saw the a session is created after the first factor, with `SecondFactorClaim` set to false, and then after the second factor is completed, we update that value to true. ## 1. Protecting all APIs We want to protect all the application APIs such that they are accessible only when `SecondFactorClaim` is `true` - indicating that the user has completed 2FA. We can do this by by overriding the `getGlobalClaimValidators` function in the Session recipe. ```tsx declare const SecondFactorClaim: BooleanClaim; // REMOVE_FROM_OUTPUT Session.init({ override: { functions: (oI) => { return { ...oI, getGlobalClaimValidators: (input) => [ ...input.claimValidatorsAddedByOtherRecipes, SecondFactorClaim.validators.hasValue(true), ], }; }, } }) ``` ```go declare const SecondFactorClaim: BooleanClaim; // REMOVE_FROM_OUTPUT let app = express(); app.post("/like-comment", verifySession({ // highlight-start overrideGlobalClaimValidators: (globalValidators) => [ ...globalValidators, SecondFactorClaim.validators.hasValue(true), ] // highlight-end }), (req: SessionRequest, res) => { //.... }); ``` ```go # Additional Verification - Multi Factor Authentication - Legacy method - Using a custom UI Source: https://supertokens.com/docs/additional-verification/mfa/legacy-mfa/frontend-custom ## 1. First factor recipe init Start by following the recipe guide for first factor login. To continue building our example app, we will use the `thirdparty` and the `emailpassword` recipes as the first factor. After following the [frontend quick setup section](/docs/quickstart/introduction) and the [social login guide](/docs/authentication/social/initial-setup), you should have the following `supertokens.init`: ```tsx SuperTokens.init({ appInfo: { apiDomain: "", apiBasePath: "", appName: "...", }, recipeList: [ Session.init(), EmailPassword.init(), ThirdParty.init() ], }); ``` ```tsx supertokens.init({ appInfo: { apiDomain: "", apiBasePath: "", appName: "...", }, recipeList: [ supertokensSession.init(), supertokensEmailPassword.init(), supertokensThirdParty.init() ], }); ``` From here on, you can continue to build out the first factor's login form using the functions exposed from the `supertokens-web-js` SDK. ## 2. Second factor recipe init For the second factor, we will be using the [passwordless recipe](https://supertokens.com/docs/passwordless/introduction). After following the [frontend quick setup section](https://supertokens.com/docs/passwordless/quick-setup/frontend), you should have the following `supertokens.init`: ```tsx // highlight-next-line SuperTokens.init({ appInfo: { apiDomain: "", apiBasePath: "", appName: "...", }, recipeList: [ Session.init(), EmailPassword.init(), ThirdParty.init(), // highlight-next-line Passwordless.init(), ], }); ``` ```tsx supertokens.init({ appInfo: { apiDomain: "", apiBasePath: "", appName: "...", }, recipeList: [ supertokensSession.init(), supertokensEmailPassword.init(), supertokensThirdParty.init(), // highlight-next-line supertokensPasswordless.init() ], }); ``` You can use the passwordless recipe function to build the second factor UI. ## 3. Reading 2FA completion information from the session for routing You will want to handle the routing of `webapp` to make sure that the correct login factor is being shown. This can be done by reading the session information. ### Checking if the first factor login should be shown ```tsx async function shouldShowFirstFactor() { return !(await Session.doesSessionExist()); } ``` ```tsx async function shouldShowFirstFactor() { return !(await supertokensSession.doesSessionExist()); } ``` If a session does not exist, this means that the user has not completed the first factor. In this case, you want to route them to the `thirdparty` + `emailpassword` login screen. ### Checking if the second factor login should be shown ```tsx // highlight-start export const SecondFactorClaim = new BooleanClaim({ id: "2fa-completed", refresh: async () => { // no-op }, }); async function shouldShowSecondFactor() { if(await shouldShowFirstFactor()) { return false; } return !(await Session.getClaimValue({ claim: SecondFactorClaim })); } // highlight-end async function shouldShowFirstFactor() { return !(await Session.doesSessionExist()); } ``` ```tsx // highlight-start // This could be moved into a separate file... export const SecondFactorClaim = new supertokensSession.BooleanClaim({ id: "2fa-completed", refresh: async () => { // This is something we have no way of refreshing, so this is a no-op }, }); async function shouldShowSecondFactor() { if(await shouldShowFirstFactor()) { return false; } if (await supertokensSession.getClaimValue({ claim: SecondFactorClaim })) { return false; } return true; } // highlight-end async function shouldShowFirstFactor() { return !(await supertokensSession.doesSessionExist()); } ``` - If a session does not exist, it means that the user has not finished the first factor yet. - If a session exists, but the `SecondFactorClaim` value is `true`, it means that the user has finished both the factors. - Otherwise the user has finished the first factor, but not the second one. ### Protecting a website route that requires both the factors You can check if a user has finished both the login factors using the two functions above: ```tsx async function areBothLoginFactorsCompleted(): Promise { // @ts-ignore return !(await shouldShowFirstFactor()) && !(await shouldShowSecondFactor()) } areBothLoginFactorsCompleted().then(async (bothFactorsCompleted) => { if (bothFactorsCompleted) { // update state to show UI } else { // @ts-ignore if (await shouldShowFirstFactor()) { // redirect user to first factor } else { // redirect user to second factor screen } } }) ``` ## 4. Getting the user's phone number for the second factor Once the user has finished the sign up process, we save their phone number in the session (as seen in the backend setup steps). This can be accessed on the frontend to send the OTP to the user without asking them to re-enter their phone after sign in: ```tsx async function getUsersPhoneNumber(): Promise { if (!(await Session.doesSessionExist())) { // the user has not finished the first factor. return undefined; } let accessTokenPayload = await Session.getAccessTokenPayloadSecurely(); if (accessTokenPayload.phoneNumber === undefined) { // this means that the user is still signing up, or it means that the user // had previously tried to sign up, but didn't complete the second factor step, // and has now just signed in. // In this case, we should ask the user to enter their phone number. return undefined; } // An OTP can be sent to this phone for the second factor. // No need to ask the user to enter their phone number again. return accessTokenPayload.phoneNumber; } ``` ```tsx async function getUsersPhoneNumber(): Promise { if (!(await supertokensSession.doesSessionExist())) { // the user has not finished the first factor. return undefined; } let accessTokenPayload = await supertokensSession.getAccessTokenPayloadSecurely(); if (accessTokenPayload.phoneNumber === undefined) { // this means that the user is still signing up, or it means that the user // had previously tried to sign up, but didn't complete the second factor step, // and has now just signed in. // In this case, we should ask the user to enter their phone number. return undefined; } // An OTP can be sent to this phone for the second factor. // No need to ask the user to enter their phone number again. return accessTokenPayload.phoneNumber; } ``` ## 5. Implementing logout If the user has completed both the factors, implementing the sign out feature can be done by: ```tsx async function signOut() { await Session.signOut(); // redirect the user to the first factor login screen } ``` ```tsx async function signOut() { await supertokensSession.signOut(); // redirect the user to the first factor login screen } ``` You should also implement a sign out button on the second factor screen, otherwise the user would be in a stuck state if they are unable to complete the second factor. To do this, you will need to call the `signOut` function as well as a function to clear the passwordless login state: ```tsx async function signOut() { // @ts-ignore if (await shouldShowSecondFactor()) { // this means we are on the second factor screen now. // calling the function below clears the login attempt info that is // saved on the browser during passwordless login. This is needed so that // future login attempts are not affected by the current one. await Passwordless.clearLoginAttemptInfo(); } await Session.signOut(); // redirect the user to the first factor login screen } ``` ```tsx async function signOut() { // @ts-ignore if (await shouldShowSecondFactor()) { // this means we are on the second factor screen now. // calling the function below clears the login attempt info that is // saved on the browser during passwordless login. This is needed so that // future login attempts are not affected by the current one. await supertokensPasswordless.clearLoginAttemptInfo(); } await supertokensSession.signOut(); // redirect the user to the first factor login screen } ``` # Additional Verification - Multi Factor Authentication - Legacy method - Using prebuilt UI - 1. Recipe init Source: https://supertokens.com/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/init :::info Caution This is the legacy method of implementing MFA. It has multiple [disadvantages](../legacy-vs-new) compared to using our MFA recipe. ::: To start, we want to initialise the [ThirdParty + EmailPassword](https://supertokens.com/docs/thirdpartyemailpassword/quick-setup/frontend), the [Passwordless](https://supertokens.com/docs/passwordless/quick-setup/frontend) and the MFA recipes: ```tsx SuperTokens.init({ appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "" }, recipeList: [ // first factor method EmailPassword.init(), ThirdParty.init({ // ... }), // second factor method Passwordless.init({ contactMethod: "PHONE" }), Session.init(), MultiFactorAuth.init({ firstFactors: ["emailpassword", "thirdparty"], }), ] }); ``` Notice that in the `MultiFactorAuth` init, we pass in the list of `firstFactors`. This will tell SuperTokens to only show email password + third party login in the pre-built UI for the first factor, even though we have also added `Passwordless.init` in the recipe list. In the subsequent sections, we will be seeing how to modify theses `init` calls to achieve the flow we want. On a high level, we will be: - Rendering the Passwordless login UI on a custom path. - Auto skipping the screen which asks the user to input their phone number if we already have it - post sign in. - Implementing a logout button on the second factor pre-built UI screen. :::info Important In the guide, we will assume that the first factor path is `/auth`, and the second factor path is `/second-factor`. ::: # Additional Verification - Multi Factor Authentication - Legacy method - Using prebuilt UI - 2. Showing the first and second factor UI Source: https://supertokens.com/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/showing-login-ui :::info Caution This is the legacy method of implementing MFA. It has multiple [disadvantages](../legacy-vs-new) compared to using our MFA recipe. ::: ## First factor UI You should see the third party + email password login UI when you visit ``. No further step is required for the first factor. ## Create and add the `SecondFactor` claim validator We will create a [custom claim](https://github.com/supertokens/supertokens-auth-react/blob/master/examples/with-legacy-2fa/src/secondFactorClaim.tsx) called `SecondFactorClaim` which will be responsible to check if the second factor has been completed or not. Then we can use the result in various places to protect frontend routes (in the subsequent steps). Create a file called `SecondFactorClaim.tsx` in which you can add the following code: ```tsx const SecondFactorClaim = new BooleanClaim({ id: "2fa-completed", refresh: async () => { // This is something we have no way of refreshing, so this is a no-op }, onFailureRedirection: () => "/second-factor", }); export default SecondFactorClaim ``` Then in the main `session.init` in the `supertokens.init` block, add this claim's validator to run the check on each route. ```tsx // @ts-ignore SuperTokens.init({ appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth" }, recipeList: [ // other recipes.. Session.init({ override: { functions: (oI) => ({ ...oI, // highlight-start getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => { return [ SecondFactorClaim.validators.isTrue(), ...claimValidatorsAddedByOtherRecipes.filter( (v) => v.id !== MultiFactorAuth.MultiFactorAuthClaim.id ), ]; }, // highlight-end }), }, }) ] }); ``` In the above, we add the `SecondFactorClaim.validators.isTrue()` validator ensuring that whenever you use `SessionAuth`, we check that the second factor claim is set to true, if not, it will redirect to `/second-factor` (as defined in the claim validator). We also remove the in build `MultiFactorAuth.MultiFactorAuthClaim` since we are not using the full in built MFA recipe (as this is the legacy method). ## Second factor UI For this guide, we will be showing the second factor UI on `/second-factor`. ### 1. Create the second factor UI on `/second-factor` Copy & paste the code for the second-factor UI from [our demo app right here](https://github.com/supertokens/supertokens-auth-react/blob/master/examples/with-legacy-2fa/src/SecondFactor/index.tsx). This component customises the `AuthPageTheme` component to: - Renders the pre-built `AuthPage` with passwordless, `otp-phone` factor. - Add a button to "login with another account" which allows users to redo the first factor. - Redirects the user to the `/` route in case the second factor has already been completed. - Auto sends the OTP to the user in case they are signing in and we already know their phone number. ### 2. Override the passwordless UI components to change the header text and disable the change phone number button Copy / Paste the following override customisations in the `Passwordless.init` function call: ```tsx function App() { return ( { if (props.factorIds.includes("otp-phone")) {
Second factor auth
; } return ; }, }}> { const session = useSessionContext(); if (session.loading !== true && session.accessTokenPayload.phoneNumber === undefined) { // this will show the change phone number button return ; } // this will hide the change phone number button return null; }, }}> {/* Rest of the JSX */}
); } export default App; ``` ### 3. Display the second factor component on your app's router Finally, we add the custom component we copy / pasted before to our router: ```tsx // @ts-ignore function App() { return (
{getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ThirdPartyPreBuiltUI, EmailPasswordPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI])} // highlight-start } /> // highlight-end
); } ``` # Additional Verification - Multi Factor Authentication - Legacy method - Using prebuilt UI - 3. Protecting routes Source: https://supertokens.com/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/protecting-routes :::info Caution This is the legacy method of implementing MFA. It has multiple [disadvantages](../legacy-vs-new) compared to using our MFA recipe. ::: Now we can wrap your application routes with the `SessionAuth` component, which should check for MFA completion by default: ```tsx // @ts-ignore // @ts-ignore function App() { return (
{getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ThirdPartyPreBuiltUI, EmailPasswordPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI])} // highlight-start } /> // highlight-end } />
); } ``` # Additional Verification - Email Verification - Enable email verification Source: https://supertokens.com/docs/additional-verification/email-verification/initial-setup ## Overview Email verification needs to be explicitly configured to work in your **SuperTokens** integration. The functionality offers two ways to set it up: - `REQUIRED`: The user needs to verify before they can access any protected routes. - `OPTIONAL`: The sessions include information about the email verification status, but it is up to you to enforce the requirement based on your business logic. ## Before you start If you are implementing [**Unified Login**](/docs/authentication/unified-login/introduction) you must manually check the `email_verified` claim on the **OAuth2 Access Tokens**. Please read the [separate page](/docs/authentication/unified-login/verify-tokens) that shows you how to verify the token. For passwordless login, with email, a user's email is automatically marked as verified when they login. Therefore, this flow only triggers if a user changes their email during a session. ## Steps ### 1. Initialize the backend recipe ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // highlight-start EmailVerification.init({ mode: "REQUIRED", // or "OPTIONAL" }), // highlight-end Session.init(), ], }); ``` ```go SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // highlight-start EmailVerification.init({ mode: "REQUIRED", // or "OPTIONAL" }), // highlight-end Session.init(), ], }); function App() { return (
// highlight-start {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [/* Other pre-built UI */ EmailVerificationPreBuiltUI])} // highlight-end // ... other routes
); } ``` ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // highlight-start EmailVerification.init({ mode: "REQUIRED", // or "OPTIONAL" }), // highlight-end Session.init(), ], }); function App() { // highlight-start if (canHandleRoute([/* Other pre-built UI */ EmailVerificationPreBuiltUI])) { return getRoutingComponent([/* Other pre-built UI */ EmailVerificationPreBuiltUI]) } // highlight-end return ( {/*Your app*/} ); } ```
You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // highlight-start supertokensUIEmailVerification.init({ mode: "REQUIRED", // or "OPTIONAL" }), // highlight-end ], }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ // highlight-next-line EmailVerification.init(), Session.init(), ], }); ```
:::important SuperTokens triggers verification emails by redirecting the user to the email verification path when the mode is `REQUIRED`. If you have set the mode to `OPTIONAL` or are **NOT** using the `SessionAuth` wrapper, you need to manually trigger the verification email. The guide on [protecting API and website routes](./protecting-routes) covers the changes that you need to make. Additionally, note that SuperTokens does not send verification emails post user sign up. Redirect the user to the email verification path to trigger the sending of the verification email. This happens automatically when using the prebuilt UI and in `REQUIRED` mode. ::: ### 2. Initialize the frontend recipe ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ // highlight-start EmailVerification.init(), Session.init(), ], }); ``` Add the following ` ``` Then call the `supertokensEmailVerification.init` function as shown below ```tsx supertokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ // highlight-start supertokensEmailVerification.init(), supertokensSession.init(), ], }); ``` :::success No specific action required here. ::: ### 3. Send the email verification email After a user signs up, or when the email verification validators fail, you need to tell the user about the email verification process. Redirect them to a screen that informs them about the current status and call the verification API. ```tsx async function sendEmail() { try { let response = await sendVerificationEmail(); if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { // This can happen if the info about email verification in the session was outdated. // Redirect the user to the home page window.location.assign("/home"); } else { // email was sent successfully. window.alert("Please check your email and click the link in it") } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function sendEmail() { try { let response = await supertokensEmailVerification.sendVerificationEmail(); if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { // This can happen if the info about email verification in the session was outdated. // Redirect the user to the home page window.location.assign("/home"); } else { // email was sent successfully. window.alert("Please check your email and click the link in it") } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` Create a new screen on your app that asks the user to enter their email to receive an email. This screen should ideally link to the sign in form. Once the user has enters their email, you can call the following API to send an email verification email to that user: ```bash curl --location --request POST '/user/email/verify/token' \ --header 'Authorization: Bearer ...' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: An email was successfully sent to the user. - `status: "EMAIL_ALREADY_VERIFIED_ERROR"`: This status can return if the info about email verification in the session was outdated. Redirect the user to the home page. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. :::info Multi Tenancy You do not need to add the tenant ID to the path here because the backend fetches the `tenantId` of the user from the session token. ::: :::note The API for sending an email verification email requires an active session. If you are using the frontend SDKs, then the session tokens should automatically get attached to the request. ::: #### Change the email verification link By default, the email verification link points to the `websiteDomain` configured on the backend. That would be the `/auth/verify-email` route if `/auth` is the value of `websiteBasePath`. If you want to change this to something different, follow the next example: ```tsx SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailVerification.init({ mode: "OPTIONAL", // highlight-start emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail(input) { return originalImplementation.sendEmail({ ...input, emailVerifyLink: input.emailVerifyLink.replace( // This is: `/verify-email` "http://localhost:3000/auth/verify-email", "http://localhost:3000/your/path" ) } ) }, } } } // highlight-end }) ] }); ``` ```go ```tsx async function consumeVerificationCode() { try { let response = await verifyEmail(); if (response.status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR") { // This can happen if the verification code is expired or invalid. // You should ask the user to retry window.alert("Oops! Seems like the verification link expired. Please try again") window.location.assign("/auth/verify-email") // back to the email sending screen. } else { // email was verified successfully. window.location.assign("/home") } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function consumeVerificationCode() { try { let response = await supertokensEmailVerification.verifyEmail(); if (response.status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR") { // This can happen if the verification code is expired or invalid. // You should ask the user to retry window.alert("Oops! Seems like the verification link expired. Please try again") window.location.assign("/auth/verify-email") // back to the email sending screen. } else { // email was verified successfully. window.location.assign("/home") } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` When the user clicks the email verification link, and it opens as a deep link into your mobile app, you can remove the token and call the verification API. ```bash curl --location --request POST '/user/email/verify' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "method": "token", "token": "ZTRiOTBjNz...jI5MTZlODkxw" }' ``` :::info Multi Tenancy For a multi tenancy setup, the `` value can fetch from `tenantId` query parameter from the email verification link. If it's not there in the link, you can use the value `"public"` (which is the default tenant). ::: The response body from the API call has a `status` property in it: - `status: "OK"`: Email verification was successful. - `status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"`: This can happen if the verification code expires or is invalid. You should ask the user to retry. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. :::caution - This API doesn't require an active session to succeed. - If you are calling the above API on page load, there is an edge case in which email clients might open the verification link in the email (for scanning purposes) and consume the token in the URL. This would lead to issues in which an attacker could sign up using someone else’s email and end up with a verified status! To prevent this, on page load, you should check if a session exists, and if it does, only then call the above API. If a session does not exist, you should first show a button, which when clicked would call the above API (email clients do not automatically click on this button). The button text could be something like "Click here to verify your email". ::: ## References ### Verification email This is how the email that the user receives looks like: UI of the verification email sent to the registered user You can find the [source code of this template on GitHub](https://github.com/supertokens/email-sms-templates/blob/master/email-html/email-verification.html) To understand more about how you can customize the check the [email delivery](/docs/platform-configuration/email-delivery) section. ### Verification link lifetime By default, the email verification link's lifetime is **1 day**. This can change via a core's configuration (time in milliseconds): - Go to the SuperTokens SaaS dashboard. - Click "Edit Configuration" on the environment you want to change. - Find the `email_verification_token_lifetime` property and change it. - Click "Save". ```bash # Here we set the lifetime to 2 hours. docker run \ -p 3567:3567 \ // highlight-next-line -e EMAIL_VERIFICATION_TOKEN_LIFETIME=7200000 \ -d registry.supertokens.io/supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command email_verification_token_lifetime: 7200000 ``` Change the email verification status manually Generate email verification links manually Protect routes based on the email verification status Post email verification action Customize the email delivery method Session claim validators # Additional Verification - Email Verification - Protecting backend and frontend routes Source: https://supertokens.com/docs/additional-verification/email-verification/protecting-routes ## Overview The `EmailVerification` claim shows the status of the email verification process. Follow this page to understand how to limit access based on whether the user has confirmed their email address. ## Before you start If you are implementing [**Unified Login**](/docs/authentication/unified-login/introduction), you must manually check the `email_verified` claim on the **OAuth2 Access Tokens**. Please read the [separate page](/docs/authentication/unified-login/verify-tokens) that shows you how to verify the token. --- ## Protect backend routes ### Add email verification checks on all routes If you want to protect all your backend API routes with email verification checks, set the `mode` to `REQUIRED` in the `EmailVerification` configuration. Routes protected with the `verifySession` middleware additionally check for email verification status. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ // highlight-start EmailVerification.init({ // This means that verifySession will now only allow calls if the user has verified their email mode: "REQUIRED", }), // highlight-end Session.init() ] }); ``` ```go let app = express(); app.post( "/update-blog", verifySession({ // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key), }), async (req: SessionRequest, res) => { // All validator checks have passed and the user has a verified email address } ); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession({ // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key), }), }, ], }, handler: async (req: SessionRequest, res) => { // All validator checks have passed and the user has a verified email address } }) ``` ```tsx let fastify = Fastify(); fastify.post("/update-blog", { preHandler: verifySession({ // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key), }), }, async (req: SessionRequest, res) => { // All validator checks have passed and the user has a verified email address }); ``` ```tsx async function updateBlog(awsEvent: SessionEvent) { // All validator checks have passed and the user has a verified email address }; exports.handler = verifySession(updateBlog, { // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key) }); ``` ```tsx let router = new KoaRouter(); router.post("/update-blog", verifySession({ // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key) }), async (ctx: SessionContext, next) => { // All validator checks have passed and the user has a verified email address }); ``` ```tsx class SetRole { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/update-blog") @intercept(verifySession({ // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key) })) @response(200) async handler() { // All validator checks have passed and the user has a verified email address } } ``` ```tsx // highlight-start export default async function setRole(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key) })(req, res, next); }, req, res ) // All validator checks have passed and the user has a verified email address } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export async function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } // We skipped checking the email verification claim return NextResponse.json({}); }, { // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key) } ); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { @Post('example') @UseGuards(new AuthGuard({ // highlight-next-line overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key) })) async postExample(@Session() session: SessionContainer): Promise { // All validator checks have passed and the user has a verified email address return true; } } ```
```go let app = express(); app.post( "/update-blog", verifySession({ // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()], }), async (req: SessionRequest, res) => { // All validator checks have passed and the user has a verified email address } ); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession({ // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()], }), }, ], }, handler: async (req: SessionRequest, res) => { // All validator checks have passed and the user has a verified email address } }) ``` ```tsx let fastify = Fastify(); fastify.post("/update-blog", { preHandler: verifySession({ // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()], }), }, async (req: SessionRequest, res) => { // All validator checks have passed and the user has a verified email address }); ``` ```tsx async function updateBlog(awsEvent: SessionEvent) { // All validator checks have passed and the user has a verified email address }; exports.handler = verifySession(updateBlog, { // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()] }); ``` ```tsx let router = new KoaRouter(); router.post("/update-blog", verifySession({ // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()] }), async (ctx: SessionContext, next) => { // All validator checks have passed and the user has a verified email address }); ``` ```tsx class SetRole { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/update-blog") @intercept(verifySession({ // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()] })) @response(200) async handler() { // All validator checks have passed and the user has a verified email address } } ``` ```tsx // highlight-start export default async function setRole(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()] })(req, res, next); }, req, res ) // All validator checks have passed and the user has a verified email address } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export async function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } // All validator checks have passed and the user has a verified email address return NextResponse.json({}); }, { // highlight-next-line overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()] } ); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { @Post('example') @UseGuards(new AuthGuard({ // highlight-next-line overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => [...globalValidators, EmailVerificationClaim.validators.isVerified()] })) async postExample(@Session() session: SessionContainer): Promise { // All validator checks have passed and the user has a verified email address return true; } } ``` ```go const VerifiedRoute = (props: React.PropsWithChildren) => { return ( {props.children} ); } function InvalidClaimHandler(props: React.PropsWithChildren) { let sessionContext = useSessionContext(); if (sessionContext.loading) { return null; } if (sessionContext.invalidClaims.some(i => i.id === EmailVerificationClaim.id)) { // Alternatively you could redirect the user to the email verification screen to trigger the verification email // Note: /auth/verify-email is the default email verification path // window.location.assign("/auth/verify-email") return
You cannot access this page because your email address is not verified.
} // We show the protected route since all claims validators have // passed implying that the user has verified their email. return
{props.children}
; } ``` In the `VerifiedRoute` component, use the `SessionAuth` wrapper to ensure that the session exists. The `` component automatically adds the `EmailVerificationClaim` validator if you initialize the `EmailVerification` recipe. Finally, check the result of the validation in the `InvalidClaimHandler` component which displays `"You cannot access this page because your email address is not verified. "` if the `EmailVerificationClaim` validator failed. Alternatively you could also redirect the user to the default email verification path to trigger the sending of the verification email. :::note You can extend the `VerifiedRoute` component to check for other types of validators as well. This component can then reuse to protect all your app's components (In this case, you may want to rename this component to something more appropriate, like `ProtectedRoute`). ::: ### Check the verification status manually If you want to have more complex access control, you can either create your own validator, or you can get the boolean from the session as follows. Check it yourself: ```tsx function ProtectedComponent() { let claimValue = Session.useClaimValue(EmailVerificationClaim) if (claimValue.loading || !claimValue.doesSessionExist) { return null; } let isEmailVerified = claimValue.value; if (isEmailVerified !== undefined && isEmailVerified) { //... } else { // Redirect the user the email verification path to send the verification email // Note: /auth/verify-email is the default email verification path window.location.assign("/auth/verify-email") } } ```
```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims(); if (validationErrors.length === 0) { // user has verified their email address return true; } else { for (const err of validationErrors) { if (err.id === EmailVerificationClaim.id) { // email is not verified } } } } // a session does not exist, or email is not verified return false } ``` In your protected routes, you need to first check if a session exists, and then call the `Session.validateClaims` function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it reflects in the `validationErrors` variable. The `EmailVerificationClaim` validator is automatically checked by this function since you have initialized the email verification recipe. ### Validation errors In case the `validationErrors` array is not empty, you can loop through the errors to know which claim has failed: ```tsx async function shouldLoadRoute() { let validationErrors = await Session.validateClaims(/*{...}*/); // highlight-start for (const err of validationErrors) { if (err.id === EmailVerificationClaim.id) { // email verification claim check failed } else { // some other claim check failed (from the global validators list) } } // highlight-end } ``` ### Check the verification status manually If you want to have more complex access control, you can either create your own validator, or you can get the boolean from the session as follows. Check it yourself: ```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { // highlight-start let isVerified = await Session.getClaimValue({claim: EmailVerificationClaim}); if (isVerified) { // user has verified their email address return true; } // highlight-end } // either a session does not exist, or the user has not verified their email address return false } ```
## Protect frontend routes ```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { // highlight-start let validationErrors = await Session.validateClaims(); if (validationErrors.length === 0) { // user has verified their email address return true; } else { for (const err of validationErrors) { if (err.id === EmailVerificationClaim.id) { // email is not verified // Send the verification email to the user await sendEmail(); } } } // highlight-end } // a session does not exist, or email is not verified return false } async function sendEmail() { try { let response = await sendVerificationEmail(); if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { // This can happen if the info about email verification in the session was outdated. // Redirect the user to the home page window.location.assign("/home"); } else { // email was sent successfully. window.alert("Please check your email and click the link in it") } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx async function shouldLoadRoute(): Promise { if (await supertokensSession.doesSessionExist()) { // highlight-start let validationErrors = await supertokensSession.validateClaims(); if (validationErrors.length === 0) { // user has verified their email address return true; } else { for (const err of validationErrors) { if (err.id === supertokensEmailVerification.EmailVerificationClaim.id) { // email is not verified // Send the verification email to the user await sendEmail(); } } } // highlight-end } // a session does not exist, or email is not verified return false } async function sendEmail() { try { let response = await supertokensEmailVerification.sendVerificationEmail(); if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { // This can happen if the info about email verification in the session was outdated. // Redirect the user to the home page window.location.assign("/home"); } else { // email was sent successfully. window.alert("Please check your email and click the link in it") } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` :::note The API for sending an email verification email requires an active session. If you are using the frontend SDKs, then the session tokens should automatically get attached to the request. ::: In your protected routes, you need to first check if a session exists, and then call the `Session.validateClaims` function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it reflects in the `validationErrors` variable. The `EmailVerificationClaim` validator is automatically checked by this function since you have initialized the email verification recipe. ### Handle 403 responses on the frontend If your frontend queries a protected API on your backend and it fails with a 403, you can call the `validateClaims` function. Loop through the errors to know which claim has failed: ```tsx async function callProtectedRoute() { try { let response = await axios.get("/protectedroute"); } catch (error) { // highlight-start if (axios.isAxiosError(error) && error.response?.status === 403) { let validationErrors = await Session.validateClaims(); for (let err of validationErrors) { if (err.id === EmailVerificationClaim.id) { // email verification claim check failed // We call the sendEmail function defined in the previous section to send the verification email. // await sendEmail(); } else { // some other claim check failed (from the global validators list) } } // highlight-end } } } ``` ```tsx async function checkIfEmailIsVerified() { if (await SuperTokens.doesSessionExist()) { // highlight-start let isVerified: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-ev"].v; if (isVerified) { // TODO.. } else { // You can trigger the sending of the verification email by calling `/user/email/verify/token` } // highlight-end } } ``` ```kotlin val isVerified: Boolean = (accessTokenPayload.get("st-ev") as JSONObject).get("v") as Boolean if (isVerified) { // TODO.. } else { // You can trigger the sending of the verification email by calling `/user/email/verify/token` } } } ``` ```swift Future checkIfEmailIsVerified() async { var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); if (accessTokenPayload.containsKey("st-ev")) { Map emailVerificationObject = accessTokenPayload["st-ev"]; if (emailVerificationObject.containsKey("v")) { bool isVerified = emailVerificationObject["v"]; if (isVerified) { // Email is verified } else { // You can trigger the sending of the verification email by calling `/user/email/verify/token` } } } } ``` ### Handle 403 responses on the frontend If your frontend queries a protected API on your backend and it fails with a 403, you can check the value of the `st-ev` claim in the access token payload. If it is false you can send the verification email --- # Additional Verification - Email Verification - Manual actions Source: https://supertokens.com/docs/additional-verification/email-verification/manual-actions ## Overview Although the **SuperTokens** covers the entire email verification process you can also intervene manually in the process. The following page shows you what SDK methods you can use to adjust the verification flow. --- ## Generate a link You can use the backend SDK to generate the email verification link as shown below: ```tsx async function createEmailVerificationLink(recipeUserId: supertokens.RecipeUserId, email: string) { try { // Create an email verification link for the user const linkResponse = await EmailVerification.createEmailVerificationLink("public", recipeUserId, email); if (linkResponse.status === "OK") { console.log(linkResponse.link); } else { // user's email is already verified } } catch (err) { console.error(err); } } ``` ```go ::: --- ## Mark the email as verified To manually mark an email as verified, you need to first create an email verification token for the user and then use the token to verify the user's email. ```tsx async function manuallyVerifyEmail(recipeUserId: supertokens.RecipeUserId) { try { // Create an email verification token for the user const tokenRes = await EmailVerification.createEmailVerificationToken("public", recipeUserId); // If the token creation is successful, use the token to verify the user's email if (tokenRes.status === "OK") { await EmailVerification.verifyEmailUsingToken("public", tokenRes.token); } } catch (err) { console.error(err); } } ``` ```go async function manuallyUnverifyEmail(recipeUserId: supertokens.RecipeUserId) { try { // Set email verification status to false await EmailVerification.unverifyEmail(recipeUserId); } catch (err) { console.error(err); } } ``` ```go # Additional Verification - Email Verification - Embed in a page Source: https://supertokens.com/docs/additional-verification/email-verification/embed-in-page ## Overview If you are looking to render the email verification UI in a different page follow this guide. ## Before you start Most of the updates require your attention if you are using the **pre-built UI** components. If you are working with a **custom UI** you need to update the backend configuration, like in step **3.1**. ## Steps ### 1. Disable the default implementation :::note no-title If you are using a **custom UI** implementation, then you can skip this step. ::: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailVerification.init({ mode: "REQUIRED", // highlight-start disableDefaultUI: true // highlight-end }), ] }); ``` If you navigate to `/auth/verify-email`, you should not see the widget anymore. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailVerification.init({ mode: "REQUIRED", // highlight-start disableDefaultUI: true // highlight-end }), ] }); ``` If you navigate to `/auth/verify-email`, you should not see the widget anymore. ### 2. Render the component yourself :::note no-title If you are using a **custom UI** implementation, then you can skip this step. ::: Add the `EmailVerification` component in your app: ```tsx // highlight-next-line class EmailVerificationPage extends React.Component { render() { return (
// highlight-next-line
) } } ```
:::caution You have to build your own UI instead. :::
### 3. Change the website path for the email verification UI {{optional}} The default path for this is component is `/{websiteBasePath}/verify-email`. If you are displaying this at some custom path, then you need add additional configuration on the backend and frontend: #### 3.1 Update the backend configuration ```tsx SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailVerification.init({ mode: "OPTIONAL", // highlight-start emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail(input) { return originalImplementation.sendEmail({ ...input, emailVerifyLink: input.emailVerifyLink.replace( // This is: `/verify-email` "http://localhost:3000/auth/verify-email", "http://localhost:3000/your/path" ) } ) }, } } } // highlight-end }) ] }); ``` ```go SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ mode: "REQUIRED", //highlight-start // The user will be taken to the custom path when they need to get their email verified. getRedirectionURL: async (context) => { if (context.action === "VERIFY_EMAIL") { return "/custom-email-verification-path"; }; } //highlight-end }) ] }) ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailVerification.init({ mode: "REQUIRED", //highlight-start // The user will be taken to the custom path when they need to get their email verified. getRedirectionURL: async (context) => { if (context.action === "VERIFY_EMAIL") { return "/custom-email-verification-path"; }; } //highlight-end }) ] }) ``` # Additional Verification - Email Verification - Customize the pre-built UI Source: https://supertokens.com/docs/additional-verification/email-verification/changing-style ## Overview Updating the CSS allows you to change the UI of the components to meet your needs. This section guides you through an example of updating the look of buttons. Note that you can apply the process to update any HTML tag from within SuperTokens components. ## Before you start This guide is only relevant if you are using the **pre-built UI** components. If you are using your own UI, you can skip this section. --- ## Global style changes Each stylable component contains the `data-supertokens` attribute (in this example `data-supertokens="link"`). For more information on how to find a specific selector look over the [changing style page](/docs/references/frontend-sdks/prebuilt-ui/changing-style). Let's add a `border` to the `link` elements. The syntax for styling is plain CSS. ```tsx preview="/img/emailverification/resend-with-border.png" previewAlt="Prebuilt form with custom submit button" SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailVerification.init({ // highlight-start style: ` [data-supertokens~=link] { border: 2px solid #0076ff; border-radius: 5; width: 30%; margin: 0 auto; } `, // highlight-end }), Session.init() ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailVerification.init({ // highlight-start style: ` [data-supertokens~=link] { border: 2px solid #0076ff; border-radius: 5px; width: 30%; margin: 0 auto; } `, // highlight-end }) ] }); ``` ### Change fonts By default, SuperTokens uses the `Arial` font. The best way to override this is to add a `font-family` styling to the `container` component in the recipe configuration. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailVerification.init({ // highlight-start style: ` [data-supertokens~=container] { font-family: cursive } ` // highlight-end }), Session.init() ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUIEmailVerification.init({ // highlight-start style: ` [data-supertokens~=container] { font-family: cursive } ` // highlight-end }), ] }); ``` ### Use media queries You may want to have different CSS for different `viewports`. You can achieve this via media queries like this: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // ... EmailVerification.init({ // ... style: ` [data-supertokens~=link] { border: 2px solid #0076ff; borderRadius: 5; width: 30%; margin: 0 auto; } // highlight-start @media (max-width: 440px) { [data-supertokens~=link] { width: 90%; } } // highlight-end `, }), ], }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // ... supertokensUIEmailVerification.init({ // ... style: ` [data-supertokens~=link] { border: 2px solid #0076ff; borderRadius: 5; width: 30%; margin: 0 auto; } // highlight-start @media (max-width: 440px) { [data-supertokens~=link] { width: 90%; } } // highlight-end `, }), ], }); ``` ## Customize individual screens ### Send email screen This screen is where the system redirects the user if you set `mode` to `REQUIRED` and they visit a path that requires a verified email. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ // highlight-start sendVerifyEmailScreen: { style: ` ... ` } // highlight-end }), Session.init() ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailVerification.init({ // highlight-start sendVerifyEmailScreen: { style: ` ... ` } // highlight-end }) ] }); ``` ### Verify link clicked screen This is the screen shown to users that click the email verification link in the email. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ // highlight-start verifyEmailLinkClickedScreen: { style: ` ... `, } // highlight-end }), Session.init() ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailVerification.init({ // highlight-start verifyEmailLinkClickedScreen: { style: ` ... `, } // highlight-end }) ] }); ``` --- # Additional Verification - Email Verification - Hooks and overrides Source: https://supertokens.com/docs/additional-verification/email-verification/hooks-and-overrides **SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. The following sections describe how you can modify adjust the `emailverification` recipe to your needs. Explore the [references pages](/docs/references) for a more in depth guide on hooks and overrides. ## Backend override To perform any task post email verification like analytics, sending a user a welcome email or notifying an internal dashboard, you need to override the `verifyEmailPOST` API. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailVerification.init({ mode: "REQUIRED", // highlight-start override: { apis: (originalImplementation) => { return { ...originalImplementation, verifyEmailPOST: async function (input) { if (originalImplementation.verifyEmailPOST === undefined) { throw Error("Should never come here"); } // First we call the original implementation let response = await originalImplementation.verifyEmailPOST(input); // Then we check if it was successfully completed if (response.status === "OK") { let { recipeUserId, email } = response.user; // TODO: post email verification logic } return response; } } } } // highlight-end }), Session.init() ] }); ``` ```go # Additional Verification - Attack Protection Suite - Initial setup Source: https://supertokens.com/docs/additional-verification/attack-protection-suite/initial-setup ## Overview The following page shows you how to include the **Attack Protection Suite** feature in your **SuperTokens** integration. ## Before you start This feature is **in beta**. To get access to it, please [reach out](mailto:support@supertokens.com) to get it set up for you. Once you have access to it, you receive: - **Public API key** - use this on your frontend for generating request IDs - **Secret API key** - use this on your backend for making requests to the anomaly detection API - **Environment ID** - use this for identifying the environment you are using both on the backend and the frontend You can use the feature with either the `Email Password` or the `Passwordless` authentication methods. For social or enterprise login, it is not needed for multiple reasons: - **Existing anomaly detection**: Most reputable third-party authentication providers (like Google, Facebook, Apple, etc.) have robust security measures in place, including their own anomaly detection systems. These systems are typically more comprehensive and tailored to their specific platforms. - **Limited visibility**: When using third-party authentication, you have limited visibility into the authentication process. This makes it difficult to accurately detect anomalies or suspicious activities that occur on the third-party's side. - **Potential false positives**: Applying anomaly detection to third-party logins might lead to an increase in false positives, as you don't have full context of the user's interactions with the third-party provider. - **User experience**: Additional security checks on top of third-party authentication could negatively impact the user experience, defeating the purpose of offering third-party login as a convenient option. ## Steps ### 1. Attach request IDs to backend API calls The **Attack Protection Suite** feature relies on identifying each request through a unique ID. This way the fingerprinting process can determine if it's a potential threat or not. :::info Important This step applies only to bot detection and anomaly IP-based detection such as impossible travel detection. Also, check for bot detection only on the email password login flows. ::: #### 1.1 Generate a request ID To generate a request ID, import, and initialize the SDK using your public API key. This SDK generates a unique request ID for each authentication event attempt. ```tsx const ENVIRONMENT_ID = ""; // Your environment ID that you received from the SuperTokens team // Initialize the agent on page load using your public API key that you received from the SuperTokens team. // @ts-expect-error const supertokensRequestIdPromise = import("https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/k9bwGCuvuA83Ad6s?apiKey=") .then((RequestId: any) => RequestId.load({ endpoint: [ 'https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/CnsdzKsyFKU8Q3h2', RequestId.defaultEndpoint ] })); async function getRequestId() { const sdk = await supertokensRequestIdPromise; const result = await sdk.get({ tag: { environmentId: ENVIRONMENT_ID, } }); return result.requestId; } ``` #### 1.2 Pass the request ID to the backend Include the `requestId` property along with the value as part of the `preAPIHook` body from the initialisation of the recipes. :::info Important If the request ID is not passed to the backend, the anomaly detection can only detect password breaches and brute force attacks. ::: Below is a full example of how to configure the SDK and pass the request ID to the backend. The request ID generates only for the email password sign in, sign up, and reset password actions because these are the only actions that require bot detection. For all the other recipes, this is not needed. ```tsx const ENVIRONMENT_ID = ""; // Your environment ID that you received from the SuperTokens team // Initialize the agent on page load using your public API key that you received from the SuperTokens team. // @ts-expect-error const supertokensRequestIdPromise = import("https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/k9bwGCuvuA83Ad6s?apiKey=") .then((RequestId: any) => RequestId.load({ endpoint: [ 'https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/CnsdzKsyFKU8Q3h2', RequestId.defaultEndpoint ] })); async function getRequestId() { const sdk = await supertokensRequestIdPromise; const result = await sdk.get({ tag: { environmentId: ENVIRONMENT_ID, } }); return result.requestId; } export const SuperTokensConfig = { // ... other config options appInfo: { appName: "...", apiDomain: '...', websiteDomain: '...', }, // recipeList contains all the modules that you want to // use from SuperTokens. See the full list here: https://supertokens.com/docs/guides recipeList: [ EmailPassword.init({ // highlight-start preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_PASSWORD_SIGN_IN" || action === "EMAIL_PASSWORD_SIGN_UP" || action === "SEND_RESET_PASSWORD_EMAIL") { let requestId = await getRequestId(); let body = context.requestInit.body; if (body !== undefined) { let bodyJson = JSON.parse(body as string); bodyJson.requestId = requestId; requestInit.body = JSON.stringify(bodyJson); } } return { requestInit, url }; } // highlight-end }), ], }; ``` ### 2. Retrieve the request ID To retrieve the request ID in the backend you have to override the recipe implementations. #### Email and password ```tsx function getIpFromRequest(req: Request): string { let headers: { [key: string]: string } = {}; for (let key of Object.keys(req.headers)) { headers[key] = (req as any).headers[key]!; } return (req as any).headers["x-forwarded-for"] || "127.0.0.1"; } const getBruteForceConfig = ( userIdentifier: string, ip: string, prefix?: string, ) => [ { key: `${prefix ? `${prefix}-` : ""}${userIdentifier}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, { key: `${prefix ? `${prefix}-` : ""}${ip}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, ]; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ // highlight-start override: { apis: (originalImplementation) => { return { ...originalImplementation, signUpPOST: async function (input) { // We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc. const requestId = (await input.options.req.getJSONBody()) .requestId; if (!requestId) { return { status: "GENERAL_ERROR", message: "The request ID is required", }; } const actionType = "emailpassword-sign-up"; const ip = getIpFromRequest(input.options.req.original); let email = input.formFields.filter((f) => f.id === "email")[0] .value as string; let password = input.formFields.filter( (f) => f.id === "password", )[0].value as string; const bruteForceConfig = getBruteForceConfig( email, ip, actionType, ); return originalImplementation.signUpPOST!(input); }, signInPOST: async function (input) { // We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc. const requestId = (await input.options.req.getJSONBody()) .requestId; if (!requestId) { return { status: "GENERAL_ERROR", message: "The request ID is required", }; } const actionType = "emailpassword-sign-up"; const ip = getIpFromRequest(input.options.req.original); let email = input.formFields.filter((f) => f.id === "email")[0] .value as string; let password = input.formFields.filter( (f) => f.id === "password", )[0].value as string; const bruteForceConfig = getBruteForceConfig( email, ip, actionType, ); return originalImplementation.signInPOST!(input); }, generatePasswordResetTokenPOST: async function (input) { // We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc. const requestId = (await input.options.req.getJSONBody()) .requestId; if (!requestId) { return { status: "GENERAL_ERROR", message: "The request ID is required", }; } const actionType = "emailpassword-sign-up"; const ip = getIpFromRequest(input.options.req.original); let email = input.formFields.filter((f) => f.id === "email")[0] .value as string; let password = input.formFields.filter( (f) => f.id === "password", )[0].value as string; const bruteForceConfig = getBruteForceConfig( email, ip, actionType, ); return originalImplementation.generatePasswordResetTokenPOST!( input, ); }, }; }, }, // highlight-end }), ], }); ``` ```go return forwardedFor } return "127.0.0.1" } func getBruteForceConfig(userIdentifier string, ip string, prefix string) []BruteForceConfig { var key string if prefix != "" { key = prefix + "-" } return []BruteForceConfig{ { Key: key + userIdentifier, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, { Key: key + ip, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, } } func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // rewrite the original implementation of SignUpPOST originalSignUpPOST := *originalImplementation.SignUpPOST (*originalImplementation.SignUpPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignUpPOSTResponse, error) { // Generate request ID for bot and suspicious IP detection var reqBody ReqBody err := json.NewDecoder(options.Req.Body).Decode(&reqBody) if err != nil { return epmodels.SignUpPOSTResponse{}, err } if reqBody.RequestID == nil { return epmodels.SignUpPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "The request ID is required", }, }, nil } requestId := *reqBody.RequestID fmt.Println(requestId) actionType := "emailpassword-sign-up" ip := getIpFromRequest(options.Req) email := "" password := "" for _, field := range formFields { if field.ID == "email" || field.ID == "password" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.SignUpPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } if field.ID == "email" { email = valueAsString } else { password = valueAsString } } } fmt.Println(password) bruteForceConfig := getBruteForceConfig(email, ip, actionType) fmt.Println(bruteForceConfig) // pre API logic... resp, err := originalSignUpPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.SignUpPOSTResponse{}, err } return resp, nil } // rewrite the original implementation of SignInPOST originalSignInPOST := *originalImplementation.SignInPOST (*originalImplementation.SignInPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignInPOSTResponse, error) { // Generate request ID for bot and suspicious IP detection var reqBody ReqBody err := json.NewDecoder(options.Req.Body).Decode(&reqBody) if err != nil { return epmodels.SignInPOSTResponse{}, err } if reqBody.RequestID == nil { return epmodels.SignInPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "The request ID is required", }, }, nil } requestId := *reqBody.RequestID fmt.Println(requestId) actionType := "emailpassword-sign-in" ip := getIpFromRequest(options.Req) email := "" password := "" for _, field := range formFields { if field.ID == "email" || field.ID == "password" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.SignInPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } if field.ID == "email" { email = valueAsString } else { password = valueAsString } } } fmt.Println(password) bruteForceConfig := getBruteForceConfig(email, ip, actionType) fmt.Println(bruteForceConfig) // pre API logic... resp, err := originalSignInPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.SignInPOSTResponse{}, err } return resp, nil } // rewrite the original implementation of GeneratePasswordResetTokenPOST originalGeneratePasswordResetTokenPOST := *originalImplementation.GeneratePasswordResetTokenPOST (*originalImplementation.GeneratePasswordResetTokenPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.GeneratePasswordResetTokenPOSTResponse, error) { // Generate request ID for bot and suspicious IP detection var reqBody ReqBody err := json.NewDecoder(options.Req.Body).Decode(&reqBody) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if reqBody.RequestID == nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "The request ID is required", }, }, nil } requestId := *reqBody.RequestID fmt.Println(requestId) actionType := "send-password-reset-email" ip := getIpFromRequest(options.Req) email := "" for _, field := range formFields { if field.ID == "email" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } email = valueAsString } } bruteForceConfig := getBruteForceConfig(email, ip, actionType) fmt.Println(bruteForceConfig) // pre API logic... resp, err := originalGeneratePasswordResetTokenPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } return resp, nil } return originalImplementation }, Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { return originalImplementation }, }, }), }, }) } ``` ```python from typing import Dict, Any, Union, List from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import APIInterface, APIOptions from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.framework import BaseRequest from supertokens_python.types import GeneralErrorResponse from supertokens_python.recipe.session import SessionContainer def get_ip_from_request(req: BaseRequest) -> str: forwarded_for = req.get_header("x-forwarded-for") if forwarded_for: return forwarded_for return "127.0.0.1" def get_brute_force_config( user_identifier: Union[str, None], ip: str, prefix: Union[str, None] = None ) -> List[Dict[str, Any]]: return [ { "key": f"{prefix}-{user_identifier}" if prefix else user_identifier, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, { "key": f"{prefix}-{ip}" if prefix else ip, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, ] # highlight-start def override_email_password_apis(original_implementation: APIInterface): original_sign_up_post = original_implementation.sign_up_post async def sign_up_post( form_fields: List[FormField], tenant_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], api_options: APIOptions, user_context: Dict[str, Any], ): request_body = await api_options.request.json() if not request_body: return GeneralErrorResponse(message="The request body is required") request_id = request_body.get("requestId") if not request_id: return GeneralErrorResponse(message="The request ID is required") action_type = "emailpassword-sign-in" ip = get_ip_from_request(api_options.request) email = None for field in form_fields: if field.id == "email": email = field.value brute_force_config = get_brute_force_config(email, ip, action_type) print(brute_force_config) response = await original_sign_up_post( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) return response original_implementation.sign_up_post = sign_up_post original_sign_in_post = original_implementation.sign_in_post async def sign_in_post( form_fields: List[FormField], tenant_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], api_options: APIOptions, user_context: Dict[str, Any], ): request_body = await api_options.request.json() if not request_body: return GeneralErrorResponse(message="The request body is required") request_id = request_body.get("requestId") if not request_id: return GeneralErrorResponse(message="The request ID is required") action_type = "emailpassword-sign-in" ip = get_ip_from_request(api_options.request) email = None for field in form_fields: if field.id == "email": email = field.value brute_force_config = get_brute_force_config(email, ip, action_type) print(brute_force_config) response = await original_sign_in_post( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) return response original_implementation.sign_in_post = sign_in_post original_generate_password_reset_token_post = ( original_implementation.generate_password_reset_token_post ) async def generate_password_reset_token_post( form_fields: List[FormField], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): request_body = await api_options.request.json() if not request_body: return GeneralErrorResponse(message="The request body is required") request_id = request_body.get("requestId") if not request_id: return GeneralErrorResponse(message="The request ID is required") action_type = "send-password-reset-email" ip = get_ip_from_request(api_options.request) email = None for field in form_fields: if field.id == "email": email = field.value brute_force_config = get_brute_force_config(email, ip, action_type) print(brute_force_config) response = await original_generate_password_reset_token_post( form_fields, tenant_id, api_options, user_context ) return response original_implementation.generate_password_reset_token_post = ( generate_password_reset_token_post ) return original_implementation # highlight-end init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ emailpassword.init( # highlight-start override=emailpassword.InputOverrideConfig( apis=override_email_password_apis ) # highlight-end ) ], ) ``` #### Passwordless ```tsx function getIpFromRequest(req: Request): string { let headers: { [key: string]: string } = {}; for (let key of Object.keys(req.headers)) { headers[key] = (req as any).headers[key]!; } return (req as any).headers["x-forwarded-for"] || "127.0.0.1"; } const getBruteForceConfig = ( userIdentifier: string, ip: string, prefix?: string, ) => [ { key: `${prefix ? `${prefix}-` : ""}${userIdentifier}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, { key: `${prefix ? `${prefix}-` : ""}${ip}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, ]; SuperTokens.init({ // @ts-ignore framework: "...", // @ts-ignore appInfo: { /*...*/ }, recipeList: [ Passwordless.init({ // ... other customisations ... // highlight-start contactMethod: "EMAIL_OR_PHONE", flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", override: { apis: (originalImplementation) => { return { ...originalImplementation, createCodePOST: async function (input) { const actionType = "passwordless-send-sms"; const ip = getIpFromRequest(input.options.req.original); const emailOrPhoneNumber = "email" in input ? input.email : input.phoneNumber; const bruteForceConfig = getBruteForceConfig( emailOrPhoneNumber, ip, actionType, ); return originalImplementation.createCodePOST!(input); }, resendCodePOST: async function (input) { const actionType = "passwordless-send-sms"; const ip = getIpFromRequest(input.options.req.original); let codesInfo = await Passwordless.listCodesByPreAuthSessionId({ tenantId: input.tenantId, preAuthSessionId: input.preAuthSessionId, }); const phoneNumber = codesInfo && "phoneNumber" in codesInfo ? codesInfo.phoneNumber : undefined; const email = codesInfo && "email" in codesInfo ? codesInfo.email : undefined; const userIdentifier = email || phoneNumber || input.deviceId; const bruteForceConfig = getBruteForceConfig( userIdentifier, ip, actionType, ); return originalImplementation.resendCodePOST!(input); }, }; }, }, // highlight-end }), ], }); ``` ```go return forwardedFor } return "127.0.0.1" } func getBruteForceConfig(userIdentifier string, ip string, prefix string) []BruteForceConfig { var key string if prefix != "" { key = prefix + "-" } return []BruteForceConfig{ { Key: key + userIdentifier, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, { Key: key + ip, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, } } func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ FlowType: "USER_INPUT_CODE", ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{ Enabled: true, }, Override: &plessmodels.OverrideStruct{ APIs: func(originalImplementation plessmodels.APIInterface) plessmodels.APIInterface { originalCreateCodePOST := *originalImplementation.CreateCodePOST (*originalImplementation.CreateCodePOST) = func(email *string, phoneNumber *string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.CreateCodePOSTResponse, error) { actionType := "passwordless-send-sms" ip := getIpFromRequest(options.Req) var key string if email != nil { key = *email } else { key = *phoneNumber } bruteForceConfig := getBruteForceConfig(key, ip, actionType) fmt.Println(bruteForceConfig) return originalCreateCodePOST(email, phoneNumber, tenantId, options, userContext) } originalResendCodePOST := *originalImplementation.ResendCodePOST (*originalImplementation.ResendCodePOST) = func(deviceID string, preAuthSessionID string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.ResendCodePOSTResponse, error) { // retreive user details codesInfo, err := passwordless.ListCodesByDeviceID(tenantId, deviceID, userContext) if err != nil { return plessmodels.ResendCodePOSTResponse{}, err } var email *string var phoneNumber *string if codesInfo.Email != nil { email = codesInfo.Email } if codesInfo.PhoneNumber != nil { phoneNumber = codesInfo.PhoneNumber } actionType := "passwordless-send-sms" ip := getIpFromRequest(options.Req) key := "" if email != nil { key = *email } else { key = *phoneNumber } bruteForceConfig := getBruteForceConfig(key, ip, actionType) fmt.Println(bruteForceConfig) return originalResendCodePOST(deviceID, preAuthSessionID, tenantId, options, userContext) } return originalImplementation }, }, }), }, }) } ``` ```python from typing import Dict, Any, Union, List from supertokens_python import init, InputAppInfo from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless.interfaces import APIInterface, APIOptions from supertokens_python.recipe.passwordless.asyncio import list_codes_by_device_id from supertokens_python.framework import BaseRequest from supertokens_python.recipe.session import SessionContainer def get_ip_from_request(req: BaseRequest) -> str: forwarded_for = req.get_header("x-forwarded-for") if forwarded_for: return forwarded_for return "127.0.0.1" def get_brute_force_config( user_identifier: Union[str, None], ip: str, prefix: Union[str, None] = None ) -> List[Dict[str, Any]]: return [ { "key": f"{prefix}-{user_identifier}" if prefix else user_identifier, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, { "key": f"{prefix}-{ip}" if prefix else ip, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, ] # highlight-start def override_passwordless_apis(original_implementation: APIInterface): original_create_code_post = original_implementation.create_code_post async def create_code_post( email: Union[str, None], phone_number: Union[str, None], session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): action_type = "passwordless-send-sms" ip = get_ip_from_request(api_options.request) identifier = None if email is not None: identifier = email elif phone_number is not None: identifier = phone_number brute_force_config = get_brute_force_config(identifier, ip, action_type) print(brute_force_config) # We need to call the original implementation of create_code_post. response = await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) return response original_implementation.create_code_post = create_code_post original_resend_code_post = original_implementation.resend_code_post async def resend_code_post( device_id: str, pre_auth_session_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): action_type = "passwordless-send-sms" ip = get_ip_from_request(api_options.request) email = None phone_number = None codes = await list_codes_by_device_id( tenant_id=tenant_id, device_id=device_id, user_context=user_context ) if codes is not None: email = codes.email phone_number = codes.phone_number identifier = None if email is not None: identifier = email elif phone_number is not None: identifier = phone_number brute_force_config = get_brute_force_config(identifier, ip, action_type) print(brute_force_config) # We need to call the original implementation of resend_code_post. response = await original_resend_code_post( device_id, pre_auth_session_id, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) return response original_implementation.resend_code_post = resend_code_post return original_implementation # highlight-end init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ passwordless.init( # highlight-start flow_type="USER_INPUT_CODE_AND_MAGIC_LINK", contact_config=passwordless.ContactEmailOrPhoneConfig(), override=passwordless.InputOverrideConfig(apis=override_passwordless_apis), # highlight-end ) ], ) ``` ### 3. Call the protection service To use the service, send requests to the appropriate regional endpoint based on your location: - **US Region (N. Virginia)**: `https://security-us-east-1.aws.supertokens.io/v1/security` - **EU Region (Ireland)**: `https://security-eu-west-1.aws.supertokens.io/v1/security` - **APAC Region (Singapore)**: `https://security-ap-southeast-1.aws.SuperTokens.io/v1/security` ```bash curl --location --request POST 'https://security-us-east-1.aws.supertokens.io/v1/security' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "user@email.com", "phoneNumber": "+1234567890", "passwordHash": "9cf95dacd226dcf43da376cdb6cbba7035218920", "requestId": "some-request-id", "actionType": "emailpassword-sign-in", "bruteForce": [ { "key": "some-key", "maxRequests": [ { "limit": 1, "perTimeIntervalMS": 1000 } ] } ] }' ``` ```tsx const REGION = "us-east-1"; // or "eu-west-1" or "ap-southeast-1" const SECRET_API_KEY = ""; const url = `https://security-${REGION}.aws.supertokens.io/v1/security`; const payload = { email: "user@email.com", phoneNumber: "+1234567890", passwordHash: "9cf95dacd226dcf43da376cdb6cbba7035218920", requestId: "some-request-id", actionType: "emailpassword-sign-in", bruteForce: [ { key: "some-key", maxRequests: [ { limit: 1, perTimeIntervalMS: 1000, }, ], }, ], }; fetch(url, { method: 'POST', headers: { 'Authorization': 'Bearer ' + SECRET_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify(payload) }) .then(response => response.json()) .then(json => console.log(json)) .catch(err => console.error(err)); ``` ```python const SECRET_API_KEY = ""; // Your secret API key that you received from the SuperTokens team // The full URL with the correct region will be provided by the SuperTokens team const ANOMALY_DETECTION_API_URL = "https://security-.aws.supertokens.io/v1/security"; async function handleSecurityChecks(input: { actionType?: string; email?: string; phoneNumber?: string; password?: string; requestId?: string; bruteForceConfig?: { key: string; maxRequests: { limit: number; perTimeIntervalMS: number; }[]; }[]; }): Promise< | { status: "GENERAL_ERROR"; message: string; } | undefined > { let requestBody: { email?: string; phoneNumber?: string; actionType?: string; requestId?: string; passwordHashPrefix?: string; bruteForce?: { key: string; maxRequests: { limit: number; perTimeIntervalMS: number; }[]; }[]; } = {}; if (input.requestId !== undefined) { requestBody.requestId = input.requestId; } let passwordHash: string | undefined; if (input.password !== undefined) { let shasum = createHash("sha1"); shasum.update(input.password); passwordHash = shasum.digest("hex"); requestBody.passwordHashPrefix = passwordHash.slice(0, 5); } requestBody.bruteForce = input.bruteForceConfig; requestBody.email = input.email; requestBody.phoneNumber = input.phoneNumber; requestBody.actionType = input.actionType; let response; try { response = await axios.post(ANOMALY_DETECTION_API_URL, requestBody, { headers: { Authorization: `Bearer ${SECRET_API_KEY}`, "Content-Type": "application/json", }, }); } catch (err) { // silently fail in order to not break the auth flow console.error(err); return; } let responseData = response.data; if (responseData.bruteForce.detected) { return { status: "GENERAL_ERROR", message: "Too many requests. Please try again later.", }; } if (responseData.requestIdInfo?.isUsingTor) { return { status: "GENERAL_ERROR", message: "Tor activity detected. Please use a regular browser.", }; } if (responseData.requestIdInfo?.vpn?.result) { return { status: "GENERAL_ERROR", message: "VPN activity detected. Please use a regular network.", }; } if (responseData.requestIdInfo?.botDetected) { return { status: "GENERAL_ERROR", message: "Bot activity detected.", }; } if (responseData?.passwordBreaches && passwordHash) { const suffix = passwordHash.slice(5).toUpperCase(); const foundPasswordHash = responseData?.passwordBreaches[suffix]; if (foundPasswordHash) { return { status: "GENERAL_ERROR", message: "This password has been detected in a breach. Please set a different password.", }; } } return undefined; } function getIpFromRequest(req: Request): string { let headers: { [key: string]: string } = {}; for (let key of Object.keys(req.headers)) { headers[key] = (req as any).headers[key]!; } return (req as any).headers["x-forwarded-for"] || "127.0.0.1"; } const getBruteForceConfig = ( userIdentifier: string, ip: string, prefix?: string, ) => [ { key: `${prefix ? `${prefix}-` : ""}${userIdentifier}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, { key: `${prefix ? `${prefix}-` : ""}${ip}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, ]; // backend SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ // highlight-start override: { apis: (originalImplementation) => { return { ...originalImplementation, signUpPOST: async function (input) { // We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc. const requestId = (await input.options.req.getJSONBody()) .requestId; if (!requestId) { return { status: "GENERAL_ERROR", message: "The request ID is required", }; } const actionType = "emailpassword-sign-up"; const ip = getIpFromRequest(input.options.req.original); let email = input.formFields.filter((f) => f.id === "email")[0] .value as string; let password = input.formFields.filter( (f) => f.id === "password", )[0].value as string; const bruteForceConfig = getBruteForceConfig( email, ip, actionType, ); // we check the anomaly detection service before calling the original implementation of signUp let securityCheckResponse = await handleSecurityChecks({ requestId, email, password, bruteForceConfig, actionType, }); if (securityCheckResponse !== undefined) { return securityCheckResponse; } return originalImplementation.signUpPOST!(input); }, signInPOST: async function (input) { // We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc. const requestId = (await input.options.req.getJSONBody()) .requestId; if (!requestId) { return { status: "GENERAL_ERROR", message: "The request ID is required", }; } const actionType = "emailpassword-sign-in"; const ip = getIpFromRequest(input.options.req.original); let email = input.formFields.filter((f) => f.id === "email")[0] .value as string; const bruteForceConfig = getBruteForceConfig( email, ip, actionType, ); // we check the anomaly detection service before calling the original implementation of signIn let securityCheckResponse = await handleSecurityChecks({ requestId, email, bruteForceConfig, actionType, }); if (securityCheckResponse !== undefined) { return securityCheckResponse; } return originalImplementation.signInPOST!(input); }, generatePasswordResetTokenPOST: async function (input) { // We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc. const requestId = (await input.options.req.getJSONBody()) .requestId; if (!requestId) { return { status: "GENERAL_ERROR", message: "The request ID is required", }; } const actionType = "send-password-reset-email"; const ip = getIpFromRequest(input.options.req.original); let email = input.formFields.filter((f) => f.id === "email")[0] .value as string; const bruteForceConfig = getBruteForceConfig( email, ip, actionType, ); // we check the anomaly detection service before calling the original implementation of generatePasswordResetToken let securityCheckResponse = await handleSecurityChecks({ requestId, email, bruteForceConfig, actionType, }); if (securityCheckResponse !== undefined) { return securityCheckResponse; } return originalImplementation.generatePasswordResetTokenPOST!( input, ); }, passwordResetPOST: async function (input) { let password = input.formFields.filter( (f) => f.id === "password", )[0].value as string; let securityCheckResponse = await handleSecurityChecks({ password, }); if (securityCheckResponse !== undefined) { return securityCheckResponse; } return originalImplementation.passwordResetPOST!(input); }, }; }, }, // highlight-end }), ], }); ``` The above code overrides the SuperTokens APIs and adding custom logic for anomaly detection. The steps when overriding the APIs are as follows: - We get the request ID from the request body. This is a unique ID for the request. - Define the action type based on the API you call. - We get the email and password from the form fields. - We get the IP address from the request. - We create the brute force configuration from the email, IP address, and action type. This configuration allows a number of requests over a time interval per: 1. Action and email/phone number. 2. Action and IP address. - We call the anomaly detection service to check if the request is permissible. - If the request is not allowed, it returns a descriptive error response. - If the request is permissible, it calls the original implementation of the API. - We return the response from the original implementation of the API. ```go // The full URL with the correct region will be provided by the SuperTokens team const ANOMALY_DETECTION_API_URL = "https://security-.aws.supertokens.io/v1/security" type SecurityCheckInput struct { ActionType string `json:"actionType,omitempty"` Email string `json:"email,omitempty"` PhoneNumber string `json:"phoneNumber,omitempty"` Password string `json:"password,omitempty"` RequestID string `json:"requestId,omitempty"` BruteForceConfig []BruteForceConfig `json:"bruteForceConfig,omitempty"` } type BruteForceConfig struct { Key string `json:"key"` MaxRequests []MaxRequests `json:"maxRequests"` } type MaxRequests struct { Limit int `json:"limit"` PerTimeIntervalMS int `json:"perTimeIntervalMS"` } type ReqBody struct { RequestID *string `json:"requestId"` } func getIpFromRequest(req *http.Request) string { if forwardedFor := req.Header.Get("X-Forwarded-For"); forwardedFor != "" { return forwardedFor } return "127.0.0.1" } func getBruteForceConfig(userIdentifier string, ip string, prefix string) []BruteForceConfig { var key string if prefix != "" { key = prefix + "-" } return []BruteForceConfig{ { Key: key + userIdentifier, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, { Key: key + ip, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, } } func handleSecurityChecks(input SecurityCheckInput) (*supertokens.GeneralErrorResponse, error) { requestBody := make(map[string]interface{}) if input.RequestID != "" { requestBody["requestId"] = input.RequestID } var passwordHash string if input.Password != "" { hash := sha1.New() hash.Write([]byte(input.Password)) passwordHash = hex.EncodeToString(hash.Sum(nil)) requestBody["passwordHashPrefix"] = passwordHash[:5] } requestBody["bruteForce"] = input.BruteForceConfig requestBody["email"] = input.Email requestBody["phoneNumber"] = input.PhoneNumber requestBody["actionType"] = input.ActionType jsonBody, err := json.Marshal(requestBody) if err != nil { return nil, err } req, err := http.NewRequest("POST", ANOMALY_DETECTION_API_URL, bytes.NewBuffer(jsonBody)) if err != nil { // silently fail in order to not break the auth flow return nil, nil } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+SECRET_API_KEY) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var responseData map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&responseData) if err != nil { return nil, err } if bruteForce, ok := responseData["bruteForce"].(map[string]interface{}); ok { if detected, ok := bruteForce["detected"].(bool); ok && detected { return &supertokens.GeneralErrorResponse{ Message: "Too many requests. Please try again later.", }, nil } } if requestIdInfo, ok := responseData["requestIdInfo"].(map[string]interface{}); ok { if isUsingTor, ok := requestIdInfo["isUsingTor"].(bool); ok && isUsingTor { return &supertokens.GeneralErrorResponse{ Message: "Tor activity detected. Please use a regular browser.", }, nil } if vpn, ok := requestIdInfo["vpn"].(map[string]interface{}); ok { if result, ok := vpn["result"].(bool); ok && result { return &supertokens.GeneralErrorResponse{ Message: "VPN activity detected. Please use a regular network.", }, nil } } if botDetected, ok := requestIdInfo["botDetected"].(bool); ok && botDetected { return &supertokens.GeneralErrorResponse{ Message: "Bot activity detected.", }, nil } } if passwordBreaches, ok := responseData["passwordBreaches"].(map[string]interface{}); ok { passwordHashSuffix := passwordHash[5:] if _, ok := passwordBreaches[passwordHashSuffix]; ok { return &supertokens.GeneralErrorResponse{ Message: "This password has been detected in a breach. Please set a different password.", }, nil } } return nil, nil } func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // rewrite the original implementation of SignUpPOST originalSignUpPOST := *originalImplementation.SignUpPOST (*originalImplementation.SignUpPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignUpPOSTResponse, error) { // Generate request ID for bot and suspicious IP detection var reqBody ReqBody err := json.NewDecoder(options.Req.Body).Decode(&reqBody) if err != nil { return epmodels.SignUpPOSTResponse{}, err } if reqBody.RequestID == nil { return epmodels.SignUpPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "The request ID is required", }, }, nil } requestId := *reqBody.RequestID actionType := "emailpassword-sign-up" ip := getIpFromRequest(options.Req) email := "" password := "" for _, field := range formFields { if field.ID == "email" || field.ID == "password" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.SignUpPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } if field.ID == "email" { email = valueAsString } else { password = valueAsString } } } bruteForceConfig := getBruteForceConfig(email, ip, actionType) // Check anomaly detection service before proceeding checkErr, err := handleSecurityChecks( SecurityCheckInput{ ActionType: actionType, Email: email, RequestID: requestId, BruteForceConfig: bruteForceConfig, Password: password, }, ) if err != nil { return epmodels.SignUpPOSTResponse{}, err } if checkErr != nil { return epmodels.SignUpPOSTResponse{ GeneralError: checkErr, }, nil } // pre API logic... resp, err := originalSignUpPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.SignUpPOSTResponse{}, err } return resp, nil } // rewrite the original implementation of SignInPOST originalSignInPOST := *originalImplementation.SignInPOST (*originalImplementation.SignInPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignInPOSTResponse, error) { // Generate request ID for bot and suspicious IP detection var reqBody ReqBody err := json.NewDecoder(options.Req.Body).Decode(&reqBody) if err != nil { return epmodels.SignInPOSTResponse{}, err } if reqBody.RequestID == nil { return epmodels.SignInPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "The request ID is required", }, }, nil } requestId := *reqBody.RequestID actionType := "emailpassword-sign-in" ip := getIpFromRequest(options.Req) email := "" password := "" for _, field := range formFields { if field.ID == "email" || field.ID == "password" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.SignInPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } if field.ID == "email" { email = valueAsString } else { password = valueAsString } } } bruteForceConfig := getBruteForceConfig(email, ip, actionType) // Check anomaly detection service before proceeding checkErr, err := handleSecurityChecks( SecurityCheckInput{ ActionType: actionType, Email: email, RequestID: requestId, BruteForceConfig: bruteForceConfig, Password: password, }, ) if err != nil { return epmodels.SignInPOSTResponse{}, err } if checkErr != nil { return epmodels.SignInPOSTResponse{ GeneralError: checkErr, }, nil } // pre API logic... resp, err := originalSignInPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.SignInPOSTResponse{}, err } return resp, nil } // rewrite the original implementation of GeneratePasswordResetTokenPOST originalGeneratePasswordResetTokenPOST := *originalImplementation.GeneratePasswordResetTokenPOST (*originalImplementation.GeneratePasswordResetTokenPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.GeneratePasswordResetTokenPOSTResponse, error) { // Generate request ID for bot and suspicious IP detection var reqBody ReqBody err := json.NewDecoder(options.Req.Body).Decode(&reqBody) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if reqBody.RequestID == nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "The request ID is required", }, }, nil } requestId := *reqBody.RequestID actionType := "send-password-reset-email" ip := getIpFromRequest(options.Req) email := "" for _, field := range formFields { if field.ID == "email" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } email = valueAsString } } bruteForceConfig := getBruteForceConfig(email, ip, actionType) // Check anomaly detection service before proceeding checkErr, err := handleSecurityChecks( SecurityCheckInput{ ActionType: actionType, Email: email, RequestID: requestId, BruteForceConfig: bruteForceConfig, }, ) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if checkErr != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{ GeneralError: checkErr, }, nil } // pre API logic... resp, err := originalGeneratePasswordResetTokenPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } return resp, nil } // rewrite the original implementation of PasswordResetPOST originalPasswordResetPOST := *originalImplementation.PasswordResetPOST (*originalImplementation.PasswordResetPOST) = func(formFields []epmodels.TypeFormField, token string, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.ResetPasswordPOSTResponse, error) { password := "" for _, field := range formFields { if field.ID == "password" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.ResetPasswordPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } password = valueAsString } } // Check anomaly detection service before proceeding checkErr, err := handleSecurityChecks( SecurityCheckInput{ Password: password, }, ) if err != nil { return epmodels.ResetPasswordPOSTResponse{}, err } if checkErr != nil { return epmodels.ResetPasswordPOSTResponse{ GeneralError: checkErr, }, nil } // First we call the original implementation resp, err := originalPasswordResetPOST(formFields, token, tenantId, options, userContext) if err != nil { return epmodels.ResetPasswordPOSTResponse{}, err } return resp, nil } return originalImplementation }, Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { return originalImplementation }, }, }), }, }) } ``` The above code overrides the SuperTokens APIs and adding custom logic for anomaly detection. The steps when overriding the APIs are as follows: - We get the request ID from the request body. This is a unique ID for the request. - Define the action type based on the API you call. - We get the email and password from the form fields. - We get the IP address from the request. - We create the brute force configuration from the email, IP address, and action type. This configuration allows a number of requests over a time interval per: 1. Action and email/phone number. 2. Action and IP address. - We call the anomaly detection service to check if the request is permissible. - If the request is not allowed, it returns a descriptive error response. - If the request is permissible, it calls the original implementation of the API. - We return the response from the original implementation of the API. ```python from httpx import AsyncClient from hashlib import sha1 from typing import Dict, Any, Union, List from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import APIInterface, APIOptions from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.framework import BaseRequest from supertokens_python.types import GeneralErrorResponse from supertokens_python.recipe.session import SessionContainer SECRET_API_KEY = "" # Your secret API key that you received from the SuperTokens team # The full URL with the correct region will be provided by the SuperTokens team ANOMALY_DETECTION_API_URL = "https://security-.aws.supertokens.io/v1/security" async def handle_security_checks( request_id: Union[str, None], password: Union[str, None], brute_force_config: Union[List[Dict[str, Any]], None], email: Union[str, None], phone_number: Union[str, None], action_type: Union[str, None], ) -> Union[GeneralErrorResponse, None]: request_body: Dict[str, Any] = {} if request_id is not None: request_body["requestId"] = request_id password_hash = None if password is not None: password_hash = sha1(password.encode()).hexdigest() request_body["passwordHashPrefix"] = password_hash[:5] request_body["bruteForce"] = brute_force_config request_body["email"] = email request_body["phoneNumber"] = phone_number request_body["actionType"] = action_type try: async with AsyncClient(timeout=10.0) as client: response = await client.post( ANOMALY_DETECTION_API_URL, json=request_body, headers={ "Authorization": f"Bearer {SECRET_API_KEY}", "Content-Type": "application/json", }, ) # type: ignore response_data = response.json() except: # silently fail in order to not break the auth flow return None if response_data.get("bruteForce", {}).get("detected"): return GeneralErrorResponse( message="Too many requests. Please try again later." ) if response_data.get("requestIdInfo", {}).get("isUsingTor"): return GeneralErrorResponse( message="Tor activity detected. Please use a regular browser." ) if response_data.get("requestIdInfo", {}).get("vpn", {}).get("result"): return GeneralErrorResponse( message="VPN activity detected. Please use a regular network." ) if response_data.get("requestIdInfo", {}).get("botDetected"): return GeneralErrorResponse(message="Bot activity detected.") if response_data.get("passwordBreaches") and password_hash is not None: password_hash_suffix = password_hash[5:] if password_hash_suffix in response_data["passwordBreaches"]: return GeneralErrorResponse( message="This password has been detected in a breach. Please set a different password." ) return None def get_ip_from_request(req: BaseRequest) -> str: forwarded_for = req.get_header("x-forwarded-for") if forwarded_for: return forwarded_for return "127.0.0.1" def get_brute_force_config( user_identifier: Union[str, None], ip: str, prefix: Union[str, None] = None ) -> List[Dict[str, Any]]: return [ { "key": f"{prefix}-{user_identifier}" if prefix else user_identifier, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, { "key": f"{prefix}-{ip}" if prefix else ip, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, ] # highlight-start def override_email_password_apis(original_implementation: APIInterface): original_sign_up_post = original_implementation.sign_up_post async def sign_up_post( form_fields: List[FormField], tenant_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], api_options: APIOptions, user_context: Dict[str, Any], ): request_body = await api_options.request.json() if not request_body: return GeneralErrorResponse(message="The request body is required") request_id = request_body.get("requestId") if not request_id: return GeneralErrorResponse(message="The request ID is required") action_type = "emailpassword-sign-in" ip = get_ip_from_request(api_options.request) email = None password = None for field in form_fields: if field.id == "email": email = field.value if field.id == "password": password = field.value brute_force_config = get_brute_force_config(email, ip, action_type) # we check the anomaly detection service before calling the original implementation of signUp security_check_response = await handle_security_checks( request_id=request_id, password=password, brute_force_config=brute_force_config, email=email, phone_number=None, action_type=action_type, ) if security_check_response is not None: return security_check_response # We need to call the original implementation of sign_up_post. response = await original_sign_up_post( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) return response original_implementation.sign_up_post = sign_up_post original_sign_in_post = original_implementation.sign_in_post async def sign_in_post( form_fields: List[FormField], tenant_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], api_options: APIOptions, user_context: Dict[str, Any], ): request_body = await api_options.request.json() if not request_body: return GeneralErrorResponse(message="The request body is required") request_id = request_body.get("requestId") if not request_id: return GeneralErrorResponse(message="The request ID is required") action_type = "emailpassword-sign-in" ip = get_ip_from_request(api_options.request) email = None for field in form_fields: if field.id == "email": email = field.value brute_force_config = get_brute_force_config(email, ip, action_type) # we check the anomaly detection service before calling the original implementation of sign_in_post security_check_response = await handle_security_checks( request_id=request_id, password=None, brute_force_config=brute_force_config, email=email, phone_number=None, action_type=action_type, ) if security_check_response is not None: return security_check_response # We need to call the original implementation of sign_in_post. response = await original_sign_in_post( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) return response original_implementation.sign_in_post = sign_in_post original_generate_password_reset_token_post = ( original_implementation.generate_password_reset_token_post ) async def generate_password_reset_token_post( form_fields: List[FormField], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): request_body = await api_options.request.json() if not request_body: return GeneralErrorResponse(message="The request body is required") request_id = request_body.get("requestId") if not request_id: return GeneralErrorResponse(message="The request ID is required") action_type = "send-password-reset-email" ip = get_ip_from_request(api_options.request) email = None for field in form_fields: if field.id == "email": email = field.value brute_force_config = get_brute_force_config(email, ip, action_type) # we check the anomaly detection service before calling the original implementation of generate_password_reset_token_post security_check_response = await handle_security_checks( request_id=request_id, password=None, brute_force_config=brute_force_config, email=email, phone_number=None, action_type=action_type, ) if security_check_response is not None: return security_check_response # We need to call the original implementation of generate_password_reset_token_post. response = await original_generate_password_reset_token_post( form_fields, tenant_id, api_options, user_context ) return response original_implementation.generate_password_reset_token_post = ( generate_password_reset_token_post ) original_password_reset_post = original_implementation.password_reset_post async def password_reset_post( form_fields: List[FormField], token: str, tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): password = None for field in form_fields: if field.id == "password": password = field.value # we check the anomaly detection service before calling the original implementation of password_reset_post security_check_response = await handle_security_checks( request_id=None, password=password, brute_force_config=None, email=None, phone_number=None, action_type=None, ) if security_check_response is not None: return security_check_response response = await original_password_reset_post( form_fields, token, tenant_id, api_options, user_context ) return response original_implementation.password_reset_post = password_reset_post return original_implementation # highlight-end init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ emailpassword.init( # highlight-start override=emailpassword.InputOverrideConfig( apis=override_email_password_apis ) # highlight-end ) ], ) ``` The above code overrides the SuperTokens APIs and adding custom logic for anomaly detection. The steps when overriding the APIs are as follows: - We get the request ID from the request body. This is a unique ID for the request. - Define the action type based on the API you call. - We get the email and password from the form fields. - We get the IP address from the request. - We create the brute force configuration from the email, IP address, and action type. This configuration allows a number of requests over a time interval per: 1. Action and email/phone number. 2. Action and IP address. - We call the anomaly detection service to check if the request is permissible. - If the request is not allowed, it returns a descriptive error response. - If the request is permissible, it calls the original implementation of the API. - We return the response from the original implementation of the API. ### Passwordless ```tsx const SECRET_API_KEY = ""; // Your secret API key that you received from the SuperTokens team const ANOMALY_DETECTION_API_URL = "https://security-us-east-1.aws.supertokens.io/v1/security"; async function handleSecurityChecks(input: { actionType?: string; email?: string; phoneNumber?: string; bruteForceConfig?: { key: string; maxRequests: { limit: number; perTimeIntervalMS: number; }[]; }[]; }): Promise< | { status: "GENERAL_ERROR"; message: string; } | undefined > { let requestBody: { email?: string; phoneNumber?: string; actionType?: string; bruteForce?: { key: string; maxRequests: { limit: number; perTimeIntervalMS: number; }[]; }[]; } = {}; requestBody.bruteForce = input.bruteForceConfig; requestBody.email = input.email; requestBody.phoneNumber = input.phoneNumber; requestBody.actionType = input.actionType; let response; try { response = await axios.post(ANOMALY_DETECTION_API_URL, requestBody, { headers: { Authorization: `Bearer ${SECRET_API_KEY}`, "Content-Type": "application/json", }, }); } catch (err) { // silently fail in order to not break the auth flow console.error(err); return; } let responseData = response.data; if (responseData.bruteForce.detected) { return { status: "GENERAL_ERROR", message: "Too many requests. Please try again later.", }; } return undefined; } function getIpFromRequest(req: Request): string { let headers: { [key: string]: string } = {}; for (let key of Object.keys(req.headers)) { headers[key] = (req as any).headers[key]!; } return (req as any).headers["x-forwarded-for"] || "127.0.0.1"; } const getBruteForceConfig = ( userIdentifier: string, ip: string, prefix?: string, ) => [ { key: `${prefix ? `${prefix}-` : ""}${userIdentifier}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, { key: `${prefix ? `${prefix}-` : ""}${ip}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, ]; SuperTokens.init({ // @ts-ignore framework: "...", // @ts-ignore appInfo: { /*...*/ }, recipeList: [ Passwordless.init({ // ... other customisations ... // highlight-start contactMethod: "EMAIL_OR_PHONE", flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", override: { apis: (originalImplementation) => { return { ...originalImplementation, createCodePOST: async function (input) { const actionType = "passwordless-send-sms"; const ip = getIpFromRequest(input.options.req.original); const emailOrPhoneNumber = "email" in input ? input.email : input.phoneNumber; const bruteForceConfig = getBruteForceConfig( emailOrPhoneNumber, ip, actionType, ); // we check the anomaly detection service before calling the original implementation of createCodePOST let securityCheckResponse = await handleSecurityChecks({ bruteForceConfig, actionType, }); if (securityCheckResponse !== undefined) { return securityCheckResponse; } return originalImplementation.createCodePOST!(input); }, resendCodePOST: async function (input) { const actionType = "passwordless-send-sms"; const ip = getIpFromRequest(input.options.req.original); let codesInfo = await Passwordless.listCodesByPreAuthSessionId({ tenantId: input.tenantId, preAuthSessionId: input.preAuthSessionId, }); const phoneNumber = codesInfo && "phoneNumber" in codesInfo ? codesInfo.phoneNumber : undefined; const email = codesInfo && "email" in codesInfo ? codesInfo.email : undefined; const userIdentifier = email || phoneNumber || input.deviceId; const bruteForceConfig = getBruteForceConfig( userIdentifier, ip, actionType, ); // we check the anomaly detection service before calling the original implementation of resendCodePOST let securityCheckResponse = await handleSecurityChecks({ phoneNumber, email, bruteForceConfig, actionType, }); if (securityCheckResponse !== undefined) { return securityCheckResponse; } return originalImplementation.resendCodePOST!(input); }, }; }, }, // highlight-end }), ], }); ``` The above code overrides the SuperTokens APIs and adding custom logic for anomaly detection. The steps when overriding the APIs are as follows: - Define the action type based on the API you call. - We get the email or the phone number from the form fields. - We get the IP address from the request. - We create the brute force configuration from the email, IP address, and action type. This configuration allows a number of requests over a time interval per: 1. Action and email/phone number. 2. Action and IP address. - The anomaly detection service checks if the request passes the allowed criteria (only brute force detection occurs here). - If the request is not allowed, the system returns a descriptive error response. - If the request passes the allowed criteria, the original implementation of the API executes. - We return the response from the original implementation of the API. ```go // The full URL with the correct region will be provided by the SuperTokens team const ANOMALY_DETECTION_API_URL = "https://security-.aws.supertokens.io/v1/security" type SecurityCheckInput struct { ActionType string `json:"actionType,omitempty"` Email string `json:"email,omitempty"` PhoneNumber string `json:"phoneNumber,omitempty"` BruteForceConfig []BruteForceConfig `json:"bruteForceConfig,omitempty"` } type BruteForceConfig struct { Key string `json:"key"` MaxRequests []MaxRequests `json:"maxRequests"` } type MaxRequests struct { Limit int `json:"limit"` PerTimeIntervalMS int `json:"perTimeIntervalMS"` } func getIpFromRequest(req *http.Request) string { if forwardedFor := req.Header.Get("X-Forwarded-For"); forwardedFor != "" { return forwardedFor } return "127.0.0.1" } func getBruteForceConfig(userIdentifier string, ip string, prefix string) []BruteForceConfig { var key string if prefix != "" { key = prefix + "-" } return []BruteForceConfig{ { Key: key + userIdentifier, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, { Key: key + ip, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, } } func handleSecurityChecks(input SecurityCheckInput) (*supertokens.GeneralErrorResponse, error) { requestBody := make(map[string]interface{}) requestBody["bruteForce"] = input.BruteForceConfig requestBody["email"] = input.Email requestBody["phoneNumber"] = input.PhoneNumber requestBody["actionType"] = input.ActionType jsonBody, err := json.Marshal(requestBody) if err != nil { return nil, err } req, err := http.NewRequest("POST", ANOMALY_DETECTION_API_URL, bytes.NewBuffer(jsonBody)) if err != nil { // silently fail in order to not break the auth flow return nil, nil } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+SECRET_API_KEY) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var responseData map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&responseData) if err != nil { return nil, err } if bruteForce, ok := responseData["bruteForce"].(map[string]interface{}); ok { if detected, ok := bruteForce["detected"].(bool); ok && detected { return &supertokens.GeneralErrorResponse{ Message: "Too many requests. Please try again later.", }, nil } } return nil, nil } func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ FlowType: "USER_INPUT_CODE", ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{ Enabled: true, }, Override: &plessmodels.OverrideStruct{ APIs: func(originalImplementation plessmodels.APIInterface) plessmodels.APIInterface { originalCreateCodePOST := *originalImplementation.CreateCodePOST (*originalImplementation.CreateCodePOST) = func(email *string, phoneNumber *string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.CreateCodePOSTResponse, error) { actionType := "passwordless-send-sms" ip := getIpFromRequest(options.Req) var key string if email != nil { key = *email } else { key = *phoneNumber } bruteForceConfig := getBruteForceConfig(key, ip, actionType) // Check anomaly detection service before proceeding checkErr, err := handleSecurityChecks( SecurityCheckInput{ ActionType: actionType, Email: *email, PhoneNumber: *phoneNumber, BruteForceConfig: bruteForceConfig, }, ) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if checkErr != nil { return plessmodels.CreateCodePOSTResponse{ GeneralError: checkErr, }, nil } return originalCreateCodePOST(email, phoneNumber, tenantId, options, userContext) } originalResendCodePOST := *originalImplementation.ResendCodePOST (*originalImplementation.ResendCodePOST) = func(deviceID string, preAuthSessionID string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.ResendCodePOSTResponse, error) { // retreive user details codesInfo, err := passwordless.ListCodesByDeviceID(tenantId, deviceID, userContext) if err != nil { return plessmodels.ResendCodePOSTResponse{}, err } var email *string var phoneNumber *string if codesInfo.Email != nil { email = codesInfo.Email } if codesInfo.PhoneNumber != nil { phoneNumber = codesInfo.PhoneNumber } actionType := "passwordless-send-sms" ip := getIpFromRequest(options.Req) key := "" if email != nil { key = *email } else { key = *phoneNumber } bruteForceConfig := getBruteForceConfig(key, ip, actionType) // Check anomaly detection service before proceeding checkErr, err := handleSecurityChecks( SecurityCheckInput{ ActionType: actionType, Email: *email, PhoneNumber: *phoneNumber, BruteForceConfig: bruteForceConfig, }, ) if err != nil { return plessmodels.ResendCodePOSTResponse{}, err } if checkErr != nil { return plessmodels.ResendCodePOSTResponse{ GeneralError: checkErr, }, nil } return originalResendCodePOST(deviceID, preAuthSessionID, tenantId, options, userContext) } return originalImplementation }, }, }), }, }) } ``` The above code overrides the SuperTokens APIs and adding custom logic for anomaly detection. The steps when overriding the APIs are as follows: - Define the action type based on the API you call. - We get the email or the phone number from the form fields. - We get the IP address from the request. - We create the brute force configuration from the email, IP address, and action type. This configuration allows a number of requests over a time interval per: 1. Action and email/phone number. 2. Action and IP address. - The anomaly detection service checks if the request passes the allowed criteria (only brute force detection occurs here). - If the request is not allowed, the system returns a descriptive error response. - If the request passes the allowed criteria, the original implementation of the API executes. - We return the response from the original implementation of the API. ```python from httpx import AsyncClient from typing import Dict, Any, Union, List from supertokens_python import init, InputAppInfo from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless.interfaces import APIInterface, APIOptions from supertokens_python.recipe.passwordless.asyncio import list_codes_by_device_id from supertokens_python.framework import BaseRequest from supertokens_python.types import GeneralErrorResponse from supertokens_python.recipe.session import SessionContainer SECRET_API_KEY = "" # Your secret API key that you received from the SuperTokens team # The full URL with the correct region will be provided by the SuperTokens team ANOMALY_DETECTION_API_URL = "https://security-.aws.supertokens.io/v1/security" async def handle_security_checks( request_id: Union[str, None], password: Union[str, None], brute_force_config: Union[List[Dict[str, Any]], None], email: Union[str, None], phone_number: Union[str, None], action_type: Union[str, None], ) -> Union[GeneralErrorResponse, None]: request_body: Dict[str, Any] = {} request_body["bruteForce"] = brute_force_config request_body["email"] = email request_body["phoneNumber"] = phone_number request_body["actionType"] = action_type try: async with AsyncClient(timeout=10.0) as client: response = await client.post( ANOMALY_DETECTION_API_URL, json=request_body, headers={ "Authorization": f"Bearer {SECRET_API_KEY}", "Content-Type": "application/json", }, ) # type: ignore response_data = response.json() except: # silently fail in order to not break the auth flow return None if response_data.get("bruteForce", {}).get("detected"): return GeneralErrorResponse( message="Too many requests. Please try again later." ) return None def get_ip_from_request(req: BaseRequest) -> str: forwarded_for = req.get_header("x-forwarded-for") if forwarded_for: return forwarded_for return "127.0.0.1" def get_brute_force_config( user_identifier: Union[str, None], ip: str, prefix: Union[str, None] = None ) -> List[Dict[str, Any]]: return [ { "key": f"{prefix}-{user_identifier}" if prefix else user_identifier, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, { "key": f"{prefix}-{ip}" if prefix else ip, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, ] # highlight-start def override_passwordless_apis(original_implementation: APIInterface): original_create_code_post = original_implementation.create_code_post async def create_code_post( email: Union[str, None], phone_number: Union[str, None], session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): action_type = "passwordless-send-sms" ip = get_ip_from_request(api_options.request) identifier = None if email is not None: identifier = email elif phone_number is not None: identifier = phone_number brute_force_config = get_brute_force_config(identifier, ip, action_type) # we check the anomaly detection service before calling the original implementation of create_code_post security_check_response = await handle_security_checks( request_id=None, password=None, brute_force_config=brute_force_config, email=email, phone_number=phone_number, action_type=action_type, ) if security_check_response is not None: return security_check_response # We need to call the original implementation of create_code_post. response = await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) return response original_implementation.create_code_post = create_code_post original_resend_code_post = original_implementation.resend_code_post async def resend_code_post( device_id: str, pre_auth_session_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): action_type = "passwordless-send-sms" ip = get_ip_from_request(api_options.request) email = None phone_number = None codes = await list_codes_by_device_id( tenant_id=tenant_id, device_id=device_id, user_context=user_context ) if codes is not None: email = codes.email phone_number = codes.phone_number identifier = None if email is not None: identifier = email elif phone_number is not None: identifier = phone_number brute_force_config = get_brute_force_config(identifier, ip, action_type) # we check the anomaly detection service before calling the original implementation of resend_code_post security_check_response = await handle_security_checks( request_id=None, password=None, brute_force_config=brute_force_config, email=email, phone_number=phone_number, action_type=action_type, ) if security_check_response is not None: return security_check_response # We need to call the original implementation of resend_code_post. response = await original_resend_code_post( device_id, pre_auth_session_id, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) return response original_implementation.resend_code_post = resend_code_post return original_implementation # highlight-end init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ passwordless.init( # highlight-start flow_type="USER_INPUT_CODE_AND_MAGIC_LINK", contact_config=passwordless.ContactEmailOrPhoneConfig(), override=passwordless.InputOverrideConfig(apis=override_passwordless_apis), # highlight-end ) ], ) ``` The above code overrides the SuperTokens APIs and adding custom logic for anomaly detection. The steps when overriding the APIs are as follows: - Define the action type based on the API you call. - We get the email or the phone number from the form fields. - We get the IP address from the request. - We create the brute force configuration from the email, IP address, and action type. This configuration allows a number of requests over a time interval per: 1. Action and email/phone number. 2. Action and IP address. - The anomaly detection service checks if the request passes the allowed criteria (only brute force detection occurs here). - If the request is not allowed, the system returns a descriptive error response. - If the request passes the allowed criteria, the original implementation of the API executes. - We return the response from the original implementation of the API. # Additional Verification - User Roles - Initial setup Source: https://supertokens.com/docs/additional-verification/user-roles/initial-setup ## Overview When you work with the `UserRoles` recipe you should follow these steps: ## Create a role and assign permissions to it ## Assign roles to users ## Protect frontend and backend routes by verifying that the user has the correct role and permissions The next sections show you the actual instructions on how to achieve this. ## Before you start :::info Multi Tenancy In a multi tenant setup, roles, and permissions share across all tenants, however, the mapping of users to roles are on a per tenant level. For example, if you create one role (`"admin"`) and add permissions to it for `read:all` and `write:all`, this role can reuse across all tenants. If you have user ID `user1` that has access to `tenant1` and `tenant2`, you can give them the `admin` role in `tenant1`, but not in `tenant2`. ::: ## Steps ### 1. Initialize the recipe ```tsx SuperTokens.init({ supertokens: { connectionURI: "..." }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ // highlight-next-line UserRoles.init(), ] }); ``` ```go UserRoles.init({ // highlight-start skipAddingRolesToAccessToken: true, skipAddingPermissionsToAccessToken: true, // highlight-end }) ``` ```go Note that if you associate a role to a user ID for a tenant, and that user ID doesn't belong to that tenant, then the operation still succeeds. ::: --- # Additional Verification - User Roles - Role management actions Source: https://supertokens.com/docs/additional-verification/user-roles/role-management-actions ## Overview **SuperTokens** exposes a set of functions and APIs that you can use to have fine-grained control over roles and permissions. Actions like listing roles, creating permissions, or checking which roles you assign are available through different SDK calls. ## Before you start :::info You can also perform most of the actions outlined on this page from the user management dashboard. To know more about how to use it check [the documentation](/docs/post-authentication/dashboard/user-management) ::: --- ## Create a role ```tsx async function createRole() { // highlight-start const response = await UserRoles.createNewRoleOrAddPermissions("user", ["read"]); if (response.createdNewRole === false) { // The role already exists } // highlight-end } ``` ```go --data-raw '{ "role": "user", "permissions": [ "read" ] }' ``` Create Role --- ## List roles ```tsx async function getAllRoles() { // highlight-start const roles: string[] = (await UserRoles.getAllRoles()).roles; // highlight-end } ``` ```go async function deleteRole() { // highlight-start // Delete the user role const response = await UserRoles.deleteRole("user"); if (!response.didRoleExist) { // There was no such role } // highlight-end } ``` ```go --data-raw '{ "role": "admin" }' ``` --- ## Add permissions The SDK function only adds missing permissions and does not have any effect on permissions that are already assigned to a role. ```tsx async function addPermissionForRole() { // highlight-start // Add the "write" permission to the "user" role await UserRoles.createNewRoleOrAddPermissions("user", ["write"]); // highlight-end } ``` ```go ```tsx async function removePermissionFromRole() { // highlight-start // Remove the "write" permission to the "user" role const response = await UserRoles.removePermissionsFromRole("user", ["write"]); if (response.status === "UNKNOWN_ROLE_ERROR") { // No such role exists } // highlight-end } ``` ```go async function getPermissionsForRole() { // highlight-start const response = await UserRoles.getPermissionsForRole("user"); if (response.status === "UNKNOWN_ROLE_ERROR") { // No such role exists return; } const permissions: string[] = response.permissions; // highlight-end } ``` ```go response, err := userroles.GetPermissionsForRole("user", nil) if err != nil { // TODO: Handle error return } if response.UnknownRoleError != nil { // No such role exists return } _ = response.OK.Permissions // highlight-end } ``` ```python from supertokens_python.recipe.userroles.asyncio import get_permissions_for_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError async def remove_permission_from_role(): # highlight-start res = await get_permissions_for_role("user") if isinstance(res, UnknownRoleError): # No such role exists return _ = res.permissions # highlight-end ``` ```python from supertokens_python.recipe.userroles.syncio import get_permissions_for_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError def remove_permission_from_role(): # highlight-start res = get_permissions_for_role("user") if isinstance(res, UnknownRoleError): # No such role exists return _ = res.permissions # highlight-end ``` --- ## Get roles by permission Get a list of all the roles assigned a specific permission. ```tsx async function getRolesWithPermission() { // highlight-start const response = await UserRoles.getRolesThatHavePermission("write"); const roles: string[] = response.roles; // highlight-end } ``` ```go Note that if you associate a role to a user ID for a tenant, and that user ID doesn't actually belong to that tenant, then the operation still succeeds. ::: --- ## Remove role from a user and their sessions You can remove roles from a user. The system removes the role you provide only if the user previously had that role. ```tsx async function removeRoleFromUserAndTheirSession(session: SessionContainer) { const response = await UserRoles.removeUserRole(session.getTenantId(), session.getUserId(), "user"); if (response.status === "UNKNOWN_ROLE_ERROR") { // No such role exists return; } if (response.didUserHaveRole === false) { // The user was never assigned the role } else { // We also want to update the session of this user to reflect this change. await session.fetchAndSetClaim(UserRoles.UserRoleClaim); await session.fetchAndSetClaim(UserRoles.PermissionClaim); } } ``` ```go --data-raw '{ "userId": "fa7a0841-b533-4478-95533-0fde890c3483", "role": "user" }' ``` :::info Multi Tenancy When using the multi-tenancy feature, in the previous snippets, only the user's role for the tenant they used to log in gets removed. That's the one stored in the session. You can pass in another tenant ID if you like, or call the function above for all the tenants that the user belongs to. ::: --- ## List the roles of a user ```tsx async function getRolesForUser(userId: string) { // highlight-start const response = await UserRoles.getRolesForUser("public", userId); const roles: string[] = response.roles; // highlight-end } ``` ```go ::: --- ## List the users of a role ```tsx async function getUsersThatHaveRole(role: string) { // highlight-start const response = await UserRoles.getUsersThatHaveRole("public", role); if (response.status === "UNKNOWN_ROLE_ERROR") { // No such role exists return; } const users: string[] = response.users; // highlight-end } ``` ```go # Additional Verification - User Roles - Protect frontend and backend routes Source: https://supertokens.com/docs/additional-verification/user-roles/protecting-routes ## Overview To limit access to your application resources based on roles and permissions you have to use the `UserRoleClaim` inside the session validation logic. ## Before you start If you are implementing [**Unified Login**](/docs/authentication/unified-login/introduction), which uses **OAuth2 Access Tokens**, please check the [separate page](/docs/authentication/unified-login/verify-tokens) that shows you how to validate them. You have to check for the `roles` claim in the token payload. --- ## Protect backend routes Override the global claim validators to integrate role verification in the standard flow. The `GlobalValidators` represents other validators that apply to all API routes by default. This may include a validator that enforces that the user has verified their email. To perform the verification follow these steps: - Add the `UserRoleClaim` validator to the `Verify Session` function which makes sure that the user has specific roles. - Optionally, add a `PermissionClaim` validator to enforce a permission. ```tsx let app = express(); app.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), async (req: SessionRequest, res) => { // All validator checks have passed and the user is an admin. } ); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), }, ], }, handler: async (req: SessionRequest, res) => { // All validator checks have passed and the user is an admin. } }) ``` ```tsx let fastify = Fastify(); fastify.post("/update-blog", { preHandler: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), }, async (req: SessionRequest, res) => { // All validator checks have passed and the user is an admin. }); ``` ```tsx async function updateBlog(awsEvent: SessionEvent) { // All validator checks have passed and the user is an admin. }; exports.handler = verifySession(updateBlog, { overrideGlobalClaimValidators: async (globalValidators) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ]) }); ``` ```tsx let router = new KoaRouter(); router.post("/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ]) }), async (ctx: SessionContext, next) => { // All validator checks have passed and the user is an admin. }); ``` ```tsx class SetRole { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/update-blog") @intercept(verifySession({ overrideGlobalClaimValidators: async (globalValidators) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ]) })) @response(200) async handler() { // All validator checks have passed and the user is an admin. } } ``` ```tsx // highlight-start export default async function setRole(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async (globalValidators) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ]) })(req, res, next); }, req, res ) // All validator checks have passed and the user is an admin. } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } // All validator checks have passed and the user is an admin. return NextResponse.json({}) }, { // highlight-start overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } // highlight-end }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { @Post('example') @UseGuards(new AuthGuard({ overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ]) })) async postExample(@Session() session: SessionContainer): Promise { // All validator checks have passed and the user is an admin. return true; } } ``` ```go let app = express(); app.post("/update-blog", verifySession(), async (req: SessionRequest, res) => { // highlight-start const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [{ id: UserRoles.UserRoleClaim.key }] }) } // highlight-end // user is an admin.. }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession() }, ], }, handler: async (req: SessionRequest, res) => { // highlight-start const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [{ id: UserRoles.UserRoleClaim.key }] }) } // highlight-end // user is an admin.. } }) ``` ```tsx let fastify = Fastify(); fastify.post("/update-blog", { preHandler: verifySession(), }, async (req: SessionRequest, res) => { // highlight-start const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [{ id: UserRoles.UserRoleClaim.key }] }) } // highlight-end // user is an admin.. }); ``` ```tsx async function updateBlog(awsEvent: SessionEvent) { // highlight-start const roles = await awsEvent.session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [{ id: UserRoles.UserRoleClaim.key }] }) } // highlight-end // user is an admin.. }; exports.handler = verifySession(updateBlog); ``` ```tsx let router = new KoaRouter(); router.post("/update-blog", verifySession(), async (ctx: SessionContext, next) => { // highlight-start const roles = await ctx.session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [{ id: UserRoles.UserRoleClaim.key }] }) } // highlight-end // user is an admin.. }); ``` ```tsx class UpdateBlog { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/update-blog") @intercept(verifySession()) @response(200) async handler() { // highlight-start const roles = await ((this.ctx as any).session as Session.SessionContainer).getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [{ id: UserRoles.UserRoleClaim.key }] }) } // highlight-end // user is an admin.. } } ``` ```tsx export default async function updateBlog(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res ) // highlight-start const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. await superTokensNextWrapper( async (next) => { throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [{ id: UserRoles.UserRoleClaim.key }] }) }, req, res ) } // highlight-end // user is an admin.. } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } const roles = await session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { const error = new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [{ id: UserRoles.UserRoleClaim.key }] }); return NextResponse.json(error, { status: 403 }); } // user is an admin.. return NextResponse.json({}) }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { @Post('example') @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise { // highlight-start const roles = await session.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [{ id: UserRoles.UserRoleClaim.key }] }) } // highlight-end // user is an admin.. return true; } } ``` ```go const AdminRoute = (props: React.PropsWithChildren) => { return ( [ ...globalValidators, UserRoleClaim.validators.includes("admin"), ] }> {props.children} ); } ``` Above, you create a generic component called `AdminRoute` which enforces that its child components render only if the user has the admin role. In the `AdminRoute` component, the `SessionAuth` wrapper ensures that the session exists. The `UserRoleClaim` validator is also added to the `` component which checks if the validators pass or not. If all validation passes, the `props.children` component renders. If the claim validation has failed, it displays the `AccessDeniedScreen` component instead of rendering the children. You can also pass a custom component to the `accessDeniedScreen` prop. :::note You can extend the `AdminRoute` component to check for other types of validators as well. This component can then reuse to protect all your app's components (In this case, you may want to rename this component to something more appropriate, like `ProtectedRoute`). ::: If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: ```tsx function ProtectedComponent() { let claimValue = Session.useClaimValue(UserRoleClaim) if (claimValue.loading || !claimValue.doesSessionExist) { return null; } let roles = claimValue.value; if (Array.isArray(roles) && roles.includes("admin")) { // User is an admin } else { // User doesn't have any roles, or is not an admin.. } } ``` ```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { // highlight-start let validationErrors = await Session.validateClaims({ overrideGlobalClaimValidators: (globalValidators) => [...globalValidators, UserRoleClaim.validators.includes("admin"), /* PermissionClaim.validators.includes("modify") */ ] }); // highlight-end if (validationErrors.length === 0) { // user is an admin return true; } for (const err of validationErrors) { if (err.id === UserRoleClaim.id) { // user roles claim check failed } else { // some other claim check failed (from the global validators list) } } } // either a session does not exist, or one of the validators failed. // so we do not allow access to this page. return false } ``` - We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. - The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that the user has verified their email (if enabled by you). - We can also add a `PermissionClaim` validator to enforce a permission. If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: ```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { // highlight-start let roles = await Session.getClaimValue({claim: UserRoleClaim}); if (Array.isArray(roles) && roles.includes("admin")) { // User is an admin return true; } // highlight-end } // either a session does not exist, or the user is not an admin return false } ``` ```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { // highlight-start let validationErrors = await Session.validateClaims({ overrideGlobalClaimValidators: (globalValidators) => [...globalValidators, UserRoleClaim.validators.includes("admin"), /* PermissionClaim.validators.includes("modify") */ ] }); // highlight-end if (validationErrors.length === 0) { // user is an admin return true; } for (const err of validationErrors) { if (err.id === UserRoleClaim.id) { // user roles claim check failed } else { // some other claim check failed (from the global validators list) } } } // either a session does not exist, or one of the validators failed. // so we do not allow access to this page. return false } ``` - We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. - The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that the user has verified their email (if enabled by you). - We can also add a `PermissionClaim` validator to enforce a permission. If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: ```tsx async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { // highlight-start let roles = await Session.getClaimValue({claim: UserRoleClaim}); if (roles !== undefined && roles.includes("admin")) { // User is an admin return true; } // highlight-end } // either a session does not exist, or the user is not an admin return false } ``` ```tsx async function shouldLoadRoute(): Promise { if (await supertokensSession.doesSessionExist()) { // highlight-start let validationErrors = await supertokensSession.validateClaims({ overrideGlobalClaimValidators: (globalValidators) => [...globalValidators, supertokensUserRoles.UserRoleClaim.validators.includes("admin"), /* supertokensUserRoles.PermissionClaim.validators.includes("modify") */ ] }); // highlight-end if (validationErrors.length === 0) { // user is an admin return true; } for (const err of validationErrors) { if (err.id === supertokensUserRoles.UserRoleClaim.id) { // user roles claim check failed } else { // some other claim check failed (from the global validators list) } } } // either a session does not exist, or one of the validators failed. // so we do not allow access to this page. return false } ``` - We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. - The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that the user has verified their email (if enabled by you). - We can also add a `PermissionClaim` validator to enforce a permission. If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: ```tsx async function shouldLoadRoute(): Promise { if (await supertokensSession.doesSessionExist()) { // highlight-start let roles = await supertokensSession.getClaimValue({claim: supertokensUserRoles.UserRoleClaim}); if (roles !== undefined && roles.includes("admin")) { // User is an admin return true; } // highlight-end } // either a session does not exist, or the user is not an admin return false } ``` ```tsx async function getRole() { if (await SuperTokens.doesSessionExist()) { // highlight-start let roles: string[] = (await SuperTokens.getAccessTokenPayloadSecurely())["st-role"].v; if (roles.includes("admin")) { // TODO.. } else { // TODO.. } // highlight-end } } ``` ```kotlin val roles: List = (accessTokenPayload.get("st-role") as JSONObject).get("v") as List; if (roles.contains("admin")) { // user is an admin } else { // user is not an admin } } } ``` ```swift Future checkIfUserIsAnAdmin() async { var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); if (accessTokenPayload.containsKey("st-role")) { Map roleObject = accessTokenPayload["st-role"]; if (roleObject.containsKey("v")) { List roles = roleObject["v"]; if (roles.contains("admin")) { // user is an admin } else { // user is not an admin } } } } ``` --- # Additional Verification - CAPTCHA validation Source: https://supertokens.com/docs/additional-verification/captcha ## Overview This following tutorial shows you how to add CAPTCHA validation to your authentication flows. The guide makes use of the plugins functionality. A new abstraction layer aimed to simplify how you can add new features in your **SuperTokens** integration. ## Before you start The plugin supports only the `React` and `NodeJS` SDKs. Support for other platforms is under active development. You can use the plugin with the following CAPTCHA providers: - [Google reCAPTCHA v2](https://developers.google.com/recaptcha/docs/display) - [Google reCAPTCHA v3](https://developers.google.com/recaptcha/docs/v3) - [Cloudflare Turnstile](https://www.cloudflare.com/en-gb/application-services/products/turnstile) Make sure to have the appropriate provider keys before starting the tutorial. The implementation is in early stages and APIs might change. For more information on how plugins work refer to the [references page](/docs/references/plugins/introduction). ## Steps ### 1. Initialize the frontend plugin #### 1.1 Install the plugin ```bash npm install @supertokens-plugins/captcha-react ``` #### 1.2 Update your frontend SDK configuration ```typescript SuperTokens.init({ appInfo: { // your app info }, recipeList: [ // your recipes ], plugins: [ CaptchaPlugin.init({ type: 'reCAPTCHAv3', // or "reCAPTCHAv2" or "turnstile" captcha: { sitekey: 'your-site-key', // Additional configuration based on the captcha provider }, }), ], }); ``` ### 2. Initialize the backend plugin #### 2.1 Install the plugin ```bash npm install @supertokens-plugins/captcha-nodejs ``` #### 2.2 Update your backend SDK configuration ```typescript SuperTokens.init({ supertokens: { connectionURI: '...', }, appInfo: { // your app info }, recipeList: [ // your recipes ], plugins: [ CaptchaPlugin.init({ type: 'reCAPTCHAv3', // or "reCAPTCHAv2" or "turnstile" captcha: { secretKey: 'your-secret-key', }, }), ], }); ``` ### 3. Customize the plugin By default, the plugin performs CAPTCHA validation on the following authentication flows: | Recipe | Authentication Flow | Forms | Pre-API Hook Action | API Function | | --------------- | -------------------------- | -------------------------------------------------------------------------------------- | --------------------------- | -------------------------------- | | `EmailPassword` | User sign in | `EmailPasswordSignInForm` | `EMAIL_PASSWORD_SIGN_IN` | `signInPOST` | | `EmailPassword` | User registration | `EmailPasswordSignUpForm` | `EMAIL_PASSWORD_SIGN_UP` | `signUpPOST` | | `EmailPassword` | Password reset request | `EmailPasswordResetPasswordEmail` | `SEND_RESET_PASSWORD_EMAIL` | `generatePasswordResetTokenPOST` | | `EmailPassword` | Password reset submission | `EmailPasswordSubmitNewPassword` | `SUBMIT_NEW_PASSWORD` | `passwordResetPOST` | | `Passwordless` | Generate verification code | `PasswordlessEmailForm` and `PasswordlessPhoneForm` and `PasswordlessEmailOrPhoneForm` | `PASSWORDLESS_CREATE_CODE` | `createCodePOST` | | `Passwordless` | Verify code and sign in | `PasswordlessUserInputForm` | `PASSWORDLESS_CONSUME_CODE` | `consumeCodePOST` | To limit which actions require additional validation, pass additional configuration parameters to the frontend and backend setup steps. #### Frontend conditional validation On the frontend create a custom component that conditionally loads the CAPTCHA provider based on the name of the form. ```typescript const CaptchaInputContainer = forwardRef< HTMLDivElement, CaptchInputContainerProps >((props, ref) => { const { form, ...rest } = props; const { loadAndRender, containerId } = useCaptcha(); useEffect(() => { // CAPTCHA applies/renders only for the EmailPasswordSignUpForm // and the EmailPasswordResetPasswordEmail if ( form === 'EmailPasswordSignUpForm' || form === 'EmailPasswordResetPasswordEmail' ) { loadAndRender(); } }, [form]); return (
); }); SuperTokens.init({ appInfo: { // your app info }, recipeList: [ // your recipes ], plugins: [ CaptchaPlugin.init({ type: 'reCAPTCHAv3', // or "reCAPTCHAv2" or "turnstile" captcha: { sitekey: 'your-site-key', }, InputContainer: CaptchaInputContainer, }), ], }); ``` #### Backend conditional validation On the backend pass a custom validation function that tells the plugin which actions should require extra validation. ```typescript const shouldValidate: ShouldValidate = (api, input) => { // Only require CAPTCHA for sign up if (api === 'signUpPOST') { return true; } // Check request headers for suspicious activity if (api === 'signInPOST') { const userAgent = input.options.req.getHeaderValue('user-agent'); return !userAgent || userAgent.includes('bot'); } return false; }; SuperTokens.init({ supertokens: { connectionURI: '...', }, appInfo: { // your app info }, recipeList: [ // your recipes ], plugins: [ CaptchaPlugin.init({ type: 'reCAPTCHAv3', // or "reCAPTCHAv2" or "turnstile" captcha: { secretKey: 'your-secret-key', }, shouldValidate, }), ], }); ``` Besides CAPTCHA validation you can also look into the **Attack Protection Suite** feature which provides prevention against suspicious authentication attempts. Attack Protection Suite Prevent suspicious authentication attempts. Multi-factor Authentication Add multi-factor authentication to your authentication flows. Plugins Reference General information on how plugins work. # Post Authentication - Post login redirection Source: https://supertokens.com/docs/post-authentication/post-login-redirect ## Change redirection path post login By default, the user is redirected to the the `/` route on your website post login. To change this, you can use the `getRedirectionURL` function on the frontend as shown below: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, // highlight-start getRedirectionURL: async (context) => { if (context.action === "SUCCESS" && context.newSessionCreated) { if (context.redirectToPath !== undefined) { // we are navigating back to where the user was before they authenticated return context.redirectToPath; } if (context.createdNewUser) { // user signed up } else { // user signed in } return "/dashboard"; } return undefined; }, // highlight-end recipeList: [ /* Recipe list */] }); ``` By default, the user is redirected the the `/` route on your website post login. To change this, you can use the `getRedirectionURL` function on the frontend as shown below: ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, // highlight-start getRedirectionURL: async (context) => { if (context.action === "SUCCESS" && context.newSessionCreated) { if (context.redirectToPath !== undefined) { // we are navigating back to where the user was before they authenticated return context.redirectToPath; } if (context.createdNewUser) { // user signed up } else { // user signed in } return "/dashboard"; } return undefined; }, // highlight-end recipeList: [ /* Recipe list */] }); ``` The user will be redirected to the provided URL on: - Successful sign up. - Successful sign in. - Successful email verification post sign up. - If the user is already logged in. If you want to redirect the user to a different domain, then you can first redirect them to a specific path using the function above, which further redirects them to the final domain. :::info Please refer to [this page](/docs/references/frontend-sdks/hooks#redirection-callback-hook) to learn more about the `getRedirectionURL` hook. ::: ## Redirect user to the login page Use the `redirectToAuth({show?: "signin" | "signup", redirectBack?: boolean}?)` function to redirect the user to the login screen. For example, you may want to call this function when the user clicks on the login button. ```tsx // highlight-next-line function NavBar () { async function onLogin () { // highlight-next-line redirectToAuth(); } return (
  • Home
  • // highlight-next-line
  • Login
) } ``` - Call `redirectToAuth({show: "signin"})` to take them to the sign in screen - Call `redirectToAuth({show: "signup"})` to take them to the sign up screen - If you do not want the user to be redirected to the current page post sign in, use `redirectToAuth({redirectBack: false})`
Redirect the user to the `/auth` (this is the default path for the pre-built UI) ```ts @Component({ selector: 'nav-bar', template: `
  • Home
  • // highlight-next-line
  • Login
`, }) export class NavBarComponent { async onLogin () { // highlight-next-line window.location.href = "/auth?show=signin&redirectToPath=" + encodeURIComponent(window.location.pathname); } } ``` - Set `show=signin` to take them to the sign in screen - Set `show=signup` to take them to the sign up screen - Set `redirectToPath` to redirect the user to a specific page after they have signed in, or you can skip it to take them to the `/` route (which is the default one).
Redirect the user to the `/auth` (this is the default path for the pre-built UI) ```html ``` - Set `show=signin` to take them to the sign in screen - Set `show=signup` to take them to the sign up screen - Set `redirectToPath` to redirect the user to a specific page after they have signed in, or you can skip it to take them to the `/` route (which is the default one).
## Showing sign up by default The login screen shows the sign in UI by default, to change that, you can set the following config: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, // highlight-next-line defaultToSignUp: true, recipeList: [ /* ... */] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, // highlight-next-line defaultToSignUp: true, recipeList: [ /* ... */] }); ``` --- # Post Authentication - Session Management - Access session data Source: https://supertokens.com/docs/post-authentication/session-management/access-session-data ## Overview The session data is accessible, both in the backend and on the frontend, after a user has successfully logged in. This guide shows you how to access different session properties. ## Before you start --- ## Access the JWT Token ### On the backend ```tsx let app = express(); app.get("/getJWT", verifySession(), async (req, res) => { let session = req.session; // highlight-start let jwt = session.getAccessToken(); // highlight-end res.json({ token: jwt }) }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/getJWT", method: "get", options: { pre: [ { method: verifySession() }, ], }, handler: async (req: SessionRequest, res) => { let session = req.session; // highlight-start let jwt = session!.getAccessToken(); // highlight-end return res.response({ token: jwt }).code(200); } }) ``` ```tsx let fastify = Fastify(); fastify.get("/getJWT", { preHandler: verifySession(), }, (req, res) => { let session = req.session; // highlight-start let jwt = session.getAccessToken(); // highlight-end res.send({ token: jwt }); }); ``` ```tsx async function getJWT(awsEvent: SessionEvent) { let session = awsEvent.session; // highlight-start let jwt = session!.getAccessToken(); // highlight-end return { body: JSON.stringify({ token: jwt }), statusCode: 200, }; }; exports.handler = verifySession(getJWT); ``` ```tsx let router = new KoaRouter(); router.get("/getJWT", verifySession(), (ctx: SessionContext, next) => { let session = ctx.session; // highlight-start let jwt = session!.getAccessToken(); // highlight-end ctx.body = { token: jwt }; }); ``` ```tsx class GetJWT { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) { } @get("/getJWT") @intercept(verifySession()) @response(200) handler() { let session = this.ctx.session; // highlight-start let jwt = session!.getAccessToken(); // highlight-end return { token: jwt }; } } ``` ```tsx export default async function getJWT(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res ) let session = req.session; // highlight-start let jwt = session!.getAccessToken(); // highlight-end res.json({ token: jwt }) } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } // highlight-start let jwt = session!.getAccessToken(); // highlight-end return NextResponse.json({ token: jwt }); }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { @Get('example') @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise<{ token: any }> { //highlight-start // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. const jwt = session.getAccessToken(); //highlight-end return { token: jwt }; } } ``` ```go SuperTokens.init({ supertokens: { connectionURI: "..." }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ //highlight-start exposeAccessTokenToFrontendInCookieBasedAuth: true, //highlight-end }) ] }); ``` ```go async function getJWT() { if (await Session.doesSessionExist()) { let userId = await Session.getUserId(); let jwt = await Session.getAccessToken(); } } ``` ```tsx async function getJWT() { if (await Session.doesSessionExist()) { let userId = await Session.getUserId(); let jwt = await Session.getAccessToken(); } } ``` ```tsx async function getJWT() { if (await Session.doesSessionExist()) { let userId = await Session.getUserId(); let jwt = await Session.getAccessToken(); } } ``` ```tsx async function getJWT() { if (await supertokensSession.doesSessionExist()) { let userId = await supertokensSession.getUserId(); let jwt = await supertokensSession.getAccessToken(); } } ``` ```tsx async function getJWT() { if (await SuperTokens.doesSessionExist()) { let userId = await SuperTokens.getUserId(); let jwt = await SuperTokens.getAccessToken(); } } ``` ```kotlin } } ``` ```swift Future getJWT() async { var jwt = await SuperTokens.getAccessToken(); if (jwt != null) { // Use `jwt` however you like } } ``` --- ## Access the Tenant ID :::info Multi Tenancy This feature is only relevant if you are using the multi tenancy feature. ::: The session's access token payload contains the tenant ID in the `tId` claim. You can access it in the following way: ### On the backend ```tsx let app = express(); // highlight-start app.post("/like-comment", verifySession(), (req: SessionRequest, res) => { let tenantId = req.session!.getTenantId(); // highlight-end //.... }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/like-comment", method: "post", //highlight-start options: { pre: [ { method: verifySession() }, ], }, handler: async (req: SessionRequest, res) => { let tenantId = req.session!.getTenantId(); //highlight-end //... } }) ``` ```tsx let fastify = Fastify(); //highlight-start fastify.post("/like-comment", { preHandler: verifySession(), }, (req: SessionRequest, res) => { let tenantId = req.session!.getTenantId(); //highlight-end //.... }); ``` ```tsx async function likeComment(awsEvent: SessionEventV2) { let tenantId = awsEvent.session!.getTenantId(); //.... }; //highlight-next-line exports.handler = verifySession(likeComment); ``` ```tsx let router = new KoaRouter(); //highlight-start router.post("/like-comment", verifySession(), (ctx: SessionContext, next) => { let tenantId = ctx.session!.getTenantId(); //highlight-end //.... }); ``` ```tsx class LikeComment { //highlight-start constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/like-comment") @intercept(verifySession()) @response(200) handler() { let tenantId = (this.ctx as SessionContext).session!.getTenantId(); //highlight-end //.... } } ``` ```tsx // highlight-start export default async function likeComment(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res ) let tenantId = req.session!.getTenantId(); // highlight-end //.... } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } let tenantId = session!.getTenantId(); //.... return NextResponse.json({}) }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { @Post('example') @UseGuards(new AuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Session() session: SessionContainer): Promise { //highlight-start let tenantId = session.getTenantId(); //highlight-end //.... return true; } } ``` ```go ::: ### On the frontend You can read the tenant ID on the frontend by adding the `tId` claim from the [access token payload](/docs/additional-verification/session-verification/claim-validation#using-the-access-token-payload). --- ## Fetch all user sessions Given a user ID, you can fetch all sessions that are active for that user in the following way: ```tsx async function getSessions() { let userId = "someUserId" // fetch somehow // sessionHandles is string[] // highlight-next-line let sessionHandles = await Session.getAllSessionHandlesForUser(userId); sessionHandles.forEach((handle) => { /* we can do the following with the handle: * - revoke this session * - change access token payload or session data * - fetch access token payload or session data */ }) } ``` ```go # Post Authentication - Session Management - Session invalidation Source: https://supertokens.com/docs/post-authentication/session-management/session-invalidation ## Overview You can invalidate a session in **SuperTokens** in different ways. The main recommendation is to use the `signOut` function from the frontend SDK. Besides that you can also revoke sessions manually, through the backend SDKs. This guide shows you how to implement each of these. ## Before you start --- ## User sign out The frontend SDK exposes a `signOut` function that revokes the session for the user. You need to add your own UI element for this since the library does not expose any components. The `signOut` function calls the sign out API exposed by the session recipe on the backend and, in turn, revokes all the user active sessions. If you call the `signOut` function whilst the access token has expired, but the refresh token still exists, the SDKs automatically perform a session refresh before revoking the session. :::important You have to add your own redirection logic after the sign out call completes. ::: ```tsx // highlight-next-line function NavBar() { async function onLogout() { // highlight-next-line await signOut(); window.location.href = "/auth"; // or redirect to wherever the login page is } return (
  • Home
  • // highlight-next-line
  • Logout
) } ```
```tsx // highlight-next-line async function logout () { // highlight-next-line await Session.signOut(); window.location.href = "/auth"; // or redirect to wherever the login page is } ```
```tsx async function logout () { // highlight-next-line await Session.signOut(); window.location.href = "/auth"; // or redirect to wherever the login page is } ``` ```tsx async function logout () { // highlight-next-line await supertokensSession.signOut(); window.location.href = "/auth"; // or redirect to wherever the login page is } ``` ```tsx async function logout () { // highlight-next-line await SuperTokens.signOut(); // navigate to the login screen.. } ``` ```kotlin // navigate to the login screen.. } } ``` ```swift Future signOut() async { await SuperTokens.signOut( completionHandler: (error) => { // Handle error if any } ); } ``` ### Expose a backend sign out method If you do not want to use the frontend function you can expose a backend sign out method. ```tsx let app = express(); // highlight-start app.post("/someapi", verifySession(), async (req: SessionRequest, res) => { // This will delete the session from the db and from the frontend (cookies) await req.session!.revokeSession(); // highlight-end res.send("Success! User session revoked"); }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/someapi", method: "post", //highlight-start options: { pre: [ { method: verifySession() }, ], }, handler: async (req: SessionRequest, res) => { // This will delete the session from the db and from the frontend (cookies) await req.session!.revokeSession(); // highlight-end return res.response("Success! User session revoked").code(200); } }) ``` ```tsx let fastify = Fastify(); //highlight-start fastify.post("/someapi", { preHandler: verifySession(), }, async (req: SessionRequest, res) => { // This will delete the session from the db and from the frontend (cookies) await req.session!.revokeSession(); // highlight-end res.send("Success! User session revoked"); }); ``` ```tsx // highlight-start async function someapi(awsEvent: SessionEvent) { // This will delete the session from the db and from the frontend (cookies) await awsEvent.session!.revokeSession(); // highlight-end return { body: JSON.stringify({ message: "Success! User session revoked" }), statusCode: 200, }; }; exports.handler = verifySession(someapi); ``` ```tsx let router = new KoaRouter(); //highlight-start router.post("/someapi", verifySession(), async (ctx: SessionContext, next) => { // This will delete the session from the db and from the frontend (cookies) await ctx.session!.revokeSession(); // highlight-end ctx.body = "Success! User session revoked"; }); ``` ```tsx class Logout { //highlight-start constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) { } @post("/someapi") @intercept(verifySession()) @response(200) async handler() { // This will delete the session from the db and from the frontend (cookies) await this.ctx.session!.revokeSession(); // highlight-end return "Success! User session revoked"; } } ``` ```tsx // highlight-start export default async function someapi(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { // highlight-next-line await verifySession()(req, res, next); }, req, res ) // This will delete the session from the db and from the frontend (cookies) await req.session!.revokeSession(); // highlight-end res.send("Success! User session revoked"); } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } // This will delete the session from the db and from the frontend (cookies) await session!.revokeSession(); return NextResponse.json({ message: "Success! User session revoked" }); }); } ``` ```ts // @ts-ignore @Controller() export class ExampleController { // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. @Post('someapi') @UseGuards(new AuthGuard()) async postSomeAPI(@Session() session: SessionContainer): Promise { await session.revokeSession(); return "Success! User session revoked"; } } ``` ```go err := sessionContainer.RevokeSession() if err != nil { err = supertokens.ErrorHandler(err, r, w) if err != nil { // TODO: Send 500 status code to client } return } // TODO: Send 200 response to client } ``` ```python from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.session import SessionContainer from fastapi import Depends from fastapi.responses import PlainTextResponse # highlight-start async def some_api(session: SessionContainer = Depends(verify_session())): await session.revoke_session() # This will delete the session from the db and from the frontend (cookies) # highlight-end return PlainTextResponse(content='success') ``` ```python from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.session import SessionContainer from flask import g # highlight-start @app.route('/some_api', methods=['POST']) # type: ignore @verify_session() def some_api(): session: SessionContainer = g.supertokens # type: ignore session.sync_revoke_session() # This will delete the session from the db and from the frontend (cookies) # highlight-end return 'success' ``` ```python from typing import cast from django.http import HttpRequest from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.django.asyncio import verify_session # highlight-start @verify_session() async def some_api(request: HttpRequest): session: SessionContainer = cast(SessionContainer, request.supertokens) # type: ignore This will delete the session from the db and from the frontend (cookies) # highlight-end await session.revoke_session() ``` :::info Tip If you are using the pre-built UI, and the `` component set custom post log out logic with the `onSessionExpired` prop. The handler gets called if: - The backend has revoked the session, but not the frontend. - The user has been inactive for too long and their refresh token has expired. ```tsx // @ts-ignore const App = () => { return ( {/* ... */ }}> ); } ``` ::: --- ## Direct session invalidation To invalidate a session without relying on the intervention of a user you can create your own custom methods using the backend SDKs. :::caution This method of revoking a session only deletes the session from the database and not from the frontend. This implies that the user can still access protected endpoints while their access token is alive. If you want to instantly logout the user in this mode, you should [enable access token blacklisting](/docs/post-authentication/session-management/advanced-workflows/access-token-blacklisting). ::: ### Revoke a specific session ```tsx async function revokeSession(sessionHandle: string) { let revoked = await Session.revokeSession(sessionHandle); }; ``` ```go let app = express(); app.use("/revoke-all-user-sessions", async (req, res) => { let userId = req.body.userId // highlight-next-line await Session.revokeAllSessionsForUser(userId); res.send("Success! All user sessions have been revoked"); }); ``` ```go # Post Authentication - Session Management - Share sessions across subdomains Source: https://supertokens.com/docs/post-authentication/session-management/share-session-across-sub-domains ## Overview Configure sharing sessions across multiple subdomains in SuperTokens by setting the `sessionTokenFrontendDomain` attribute of the Session recipe in your frontend code. :::info Example - Your app has two subdomains `abc.example.com` and `xyz.example.com`. Assume that the user logs in via `example.com` - To enable sharing sessions across `example.com`, `abc.example.com` and `xyz.example.com`, set the `sessionTokenFrontendDomain` attribute to `.example.com`. ::: ## Steps ### 1. Update the frontend configuration ```tsx SuperTokens.init({ appInfo: { // ... // this should be equal to the domain where the user will see the login UI apiDomain: "...", appName: "...", websiteDomain: "https://example.com" }, recipeList: [ Session.init({ // highlight-next-line sessionTokenFrontendDomain: ".example.com" }) ] }); ``` You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx supertokensUIInit({ appInfo: { // ... // this should be equal to the domain where the user will see the login UI apiDomain: "...", appName: "...", websiteDomain: "https://example.com" }, recipeList: [ supertokensUISession.init({ // highlight-next-line sessionTokenFrontendDomain: ".example.com" }) ] }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ // highlight-start sessionTokenFrontendDomain: ".example.com" // highlight-end }), ], }) ``` :::caution - Do not set `sessionTokenFrontendDomain` to a value that's in the [public suffix list](https://publicsuffix.org/list/public_suffix_list.dat) (Search for your value without the leading dot). Otherwise, session management does not work. - Do not set `sessionTokenFrontendDomain` to `.localhost` or an IP address based domain with a leading `.` since browsers reject these cookies. For local development, you should configure [your machine to use alias for `localhost`](https://superuser.com/questions/152146/how-to-alias-a-hostname-on-mac-osx). ::: :::info Multi Tenancy If each tenant belongs to one subdomain, and a user has access to more than one tenant, the tenant ID in the session is always the one from which they logged in. For example, if a user has access to tenant `t1.example.com` and `t2.example.com`, and they logged in via `t1.example.com`, then the tenant ID in the session is always `t1`. This remains true even if they navigate to `t2.example.com` or make an API request from `t2.example.com`. To solve this, add extra information about access token payload containing a list of all the tenants that the user has access to. Then read from that list instead of the `tId` claim. ::: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ // highlight-start sessionTokenFrontendDomain: ".example.com" // highlight-end }), ], }) ``` ```tsx supertokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ supertokensSession.init({ // ... // highlight-start sessionTokenFrontendDomain: ".example.com" // highlight-end }) ], }) ``` :::caution - Do not set `sessionTokenFrontendDomain` to a value that's in the [public suffix list](https://publicsuffix.org/list/public_suffix_list.dat) (Search for your value without the leading dot). Otherwise, session management does not work. - Do not set `sessionTokenFrontendDomain` to `.localhost` or an IP address based domain with a leading `.` since browsers reject these cookies. For local development, you should configure [your machine to use alias for `localhost`](https://superuser.com/questions/152146/how-to-alias-a-hostname-on-mac-osx). ::: :::info Multi Tenancy If each tenant belongs to one subdomain, and a user has access to more than one tenant, the tenant ID in the session is always the one from which they logged in. For example, if a user has access to tenant `t1.example.com` and `t2.example.com`, and they logged in via `t1.example.com`, then the tenant ID in the session is always `t1`. This remains true even if they navigate to `t2.example.com` or make an API request from `t2.example.com`. To solve this, add extra information about access token payload containing a list of all the tenants that the user has access to. Then read from that list instead of the `tId` claim. ::: :::caution Not applicable ::: --- # Post Authentication - Session Management - Switch between cookie and header-based sessions Source: https://supertokens.com/docs/post-authentication/session-management/switch-between-cookies-and-header-authentication ## Overview SuperTokens supports 2 methods of authorizing requests. The following guide shows you how to switch between them. ### Cookie based - The default in the web SDKs - Uses [`HttpOnly` cookies](https://owasp.org/www-community/HttpOnly) by default to prevent token theft via XSS ### Header based - The default in the mobile SDKs - Uses the [`Authorization` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) with a [`Bearer` auth-scheme](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes) - This can make it easier to work with API gateways and third-party services - Preferable in mobile environments, since they can have buggy and/or unreliable cookie implementations When creating or authorising sessions, the SDK has to choose to send the tokens to the frontend by cookies or custom headers. The backend controls this choice, but it follows a preference set in the frontend configuration. ## Before you start :::caution We recommend cookie-based sessions in browsers because header-based sessions require saving the access and refresh tokens in storage vulnerable to XSS attacks. ::: ## Steps ### 1. Update the frontend configuration You can provide a `tokenTransferMethod` property in the configuration of the Session recipe to set the preferred token transfer method. The backend receives this method with every request in the `st-auth-mode` header. By default, the backend follows this preference. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ // highlight-next-line tokenTransferMethod: "header" // or "cookie" }) ] }); ``` You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUISession.init({ // highlight-next-line tokenTransferMethod: "header" // or "cookie" }) ] }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ // highlight-next-line tokenTransferMethod: "header" // or "cookie" }) ], }) ``` ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ // highlight-next-line tokenTransferMethod: "header" // or "cookie" }) ], }) ``` ```tsx supertokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ supertokensSession.init({ // highlight-next-line tokenTransferMethod: "header" // or "cookie", }) ], }) ``` ```tsx SuperTokens.init({ apiDomain: "...", tokenTransferMethod: "header", // or "cookie". "header" by default }); ``` You can use the `tokenTransferMethod` builder method to set what mode the SDK should use for sessions. ```kotlin ```swift void main() { SuperTokens.init( apiDomain: "...", tokenTransferMethod: SuperTokensTokenTransferMethod.COOKIE, ); } ``` ### 2. Update the backend configuration {{optional}} This step is optional. You can force the backend to use a specific token transfer method regardless of the frontend configuration. :::caution **You should not set this on the backend if you have more than one client using different modes** (for example if you have a website that uses cookie based, and a mobile app that uses header based sessions). ::: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ // highlight-start getTokenTransferMethod: () => "header", // highlight-end }) ] }); ``` ```go # Post Authentication - Session Management - Advanced Workflows - Implement anonymous sessions Source: https://supertokens.com/docs/post-authentication/session-management/advanced-workflows/anonymous-session ## Overview With anonymous sessions, you can keep track of user's action / data before they login, and then transfer that data to their post login session. Anonymous sessions have different properties than regular, logged in sessions: - The `userID` of anonymous sessions doesn't matter - The security constraints on anonymous sessions are lesser than regular sessions, as you would want users to log in before doing anything sensitive anyway. - Each visitor that visits your app / website gets an anonymous session if they don't have one previously. This does not require them to log in. - Anonymous sessions are not stored in the database to avoid the risk of flooding the database with sessions that are not useful to the app. Given the different characteristics of anonymous sessions, using a simple, long lived JWT is a perfect use case. They can store any information about the user's activity, and they don't occupy any database space either. ## Steps ### 1. Create the JWT Start by creating a JWT like in the next example: ```tsx async function createAnonymousJWT(payload: any) { let jwtResponse = await Session.createJWT({ key: "value", // more payload... }, 315360000); // 10 years lifetime if (jwtResponse.status === "OK") { // Send JWT as Authorization header to M2 return jwtResponse.jwt; } throw new Error("Unable to create JWT. Should never come here.") } ``` ```go The `verifySession` and `getSession` check for the presence of certain claims in the JWT (`sessionHandle`, `refreshTokenHash` etc...) that the Session recipe adds for authenticated users. ::: ### 3. Transfer the data to a logged in session The idea here is to override the `Session` recipe on the `backend`. Whenever the user logs in or signs up, the data from the anonymous session transfers to the logged-in session. The override attempts to read the request header cookie to get the JWT. It assumes that you have added it to the cookies, verifies it, and then adds the payload to the logged-in session. ```tsx SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ // ... Session.init({ // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, createNewSession: async function (input) { let userId = input.userId; const request = SuperTokens.getRequestFromUserContext(input.userContext); let jwt: string | undefined let jwtPayload = {} if (request !== undefined) { jwt = request.getCookieValue("jwt"); } else { /** * This is possible if the function is triggered from the user management dashboard * * In this case because we cannot read the JWT, we create a session without the custom * payload properties */ } if (jwt !== undefined) { // verify JWT using a JWT verification library.. jwtPayload = { /* ... get from decoded jwt ... */}; } // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, ...jwtPayload }; return originalImplementation.createNewSession(input); }, }; }, }, // highlight-end }) ] }); ``` ```go } // This goes in the access token, and is available to read on the frontend. if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } accessTokenPayload["someKey"] = decodedJWT["someKey"] } } else { /** * This is possible if the function is triggered from the user management dashboard * * In this case because we cannot read the JWT, we create a session without the custom * payload properties */ } return originalCreateNewSession(userID, accessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext) } return originalImplementation }, }, // highlight-end }), }, }) } ``` ```python from supertokens_python import init, InputAppInfo, get_request_from_user_context from supertokens_python.recipe import session from supertokens_python.recipe.session.interfaces import RecipeInterface from typing import Any, Dict, Optional from supertokens_python.types import RecipeUserId def override_functions(original_implementation: RecipeInterface): original_implementation_create_new_session = ( original_implementation.create_new_session ) async def create_new_session( user_id: str, recipe_user_id: RecipeUserId, access_token_payload: Optional[Dict[str, Any]], session_data_in_database: Optional[Dict[str, Any]], disable_anti_csrf: Optional[bool], tenant_id: str, user_context: Dict[str, Any], ): request = get_request_from_user_context(user_context) jwt: Optional[str] = None if request is not None: jwt = request.get_cookie("jwt") else: # # This is possible if the function is triggered from the user management dashboard # # In this case because we cannot read the JWT, we create a session without the custom # payload properties # jwt = None if jwt is not None: # verify JWT using a JWT verification library.. jwt_payload = { # from JWT verification lib } # This goes in the access token, and is available to read on the frontend. if access_token_payload is None: access_token_payload = {} access_token_payload["someKey"] = jwt_payload["someKey"] return await original_implementation_create_new_session( user_id, recipe_user_id, access_token_payload, session_data_in_database, disable_anti_csrf, tenant_id, user_context, ) original_implementation.create_new_session = create_new_session return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ session.init( # highlight-start override=session.InputOverrideConfig(functions=override_functions) # highlight-end ) ], ) ``` - In the above code snippet, the system reads the JWT from the request header cookie. If it exists, it verifies it and then adds the payload to the logged-in session. - If the JWT doesn't exist, or if the system cannot verify it, it would be safe to ignore it and create the logged-in session anyway. - We assume that the you have saved the JWT in the cookies in with the key of `jwt`. But if not, you can remove the JWT from the request based on how you have saved it. You can even read the headers from the request. # Post Authentication - Session Management - Advanced Workflows - Implement user impersonation Source: https://supertokens.com/docs/post-authentication/session-management/advanced-workflows/user-impersonation ## Overview Impersonating a user allows you to login as them without using their credentials. This is useful for testing purposes, or for customer support. This guide shows you how to achieve this by only allowing a certain type of users, `admins`, to perform the impersonation. ## Before you start :::caution Since this feature allows you to login as any user in your application, only admins or custom support staff should use it. You can use any method to protect this API. (API Key, specific user roles, IP address validation, etc.) ::: ## Steps ### 1. Create the impersonation endpoint Create a new API endpoint that accepts some form of identifier (email, phone number, user ID, etc.) and creates a new impersonation session for that user. In order for this to work, admins need to first log in to the application as themselves. Once they create their session (like any regular user's session), they can call the API via a frontend UI that's only shown to them. You can detect the admin role on the frontend by seeing [this guide](/docs/additional-verification/user-roles/protecting-routes#protecting-frontend-routes). ```tsx let app = express(); app.post( "/impersonate", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin") ], }), async (req, res) => { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email }); if (user.length === 0) { throw new Error("User does not exist"); } await Session.createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); res.json({ message: "Impersonation successful!" }); }) ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/impersonate", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin") ], }), }, ], }, handler: async (req, res) => { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email }); if (user.length === 0) { throw new Error("User does not exist"); } await Session.createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); return res.response({ message: "Impersonation successful!" }).code(200); }, }); ``` ```tsx let fastify = Fastify(); fastify.post( "/impersonate", { preHandler: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin") ], }), }, async (req, res) => { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email }); if (user.length === 0) { throw new Error("User does not exist"); } await Session.createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); res.send({ message: "Impersonation successful!" }); }) ``` ```tsx async function impersonate(awsEvent: SessionEvent) { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email }); if (user.length === 0) { throw new Error("User does not exist"); } await Session.createNewSession(awsEvent, awsEvent, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); return { body: JSON.stringify({ message: "Impersonation successful!" }), statusCode: 200, }; } exports.handler = verifySession(impersonate, { overrideGlobalClaimValidators: async (globalValidators) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin") ]) }); ``` ```tsx let router = new KoaRouter(); router.post( "/impersonate", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin") ]) }), async (ctx, next) => { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email }); if (user.length === 0) { throw new Error("User does not exist"); } await Session.createNewSession(ctx, ctx, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); ctx.body = { message: "Impersonation successful!" }; }) ``` ```tsx class Login { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/impersonate") @intercept(verifySession({ overrideGlobalClaimValidators: async (globalValidators) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin") ]) })) @response(200) async handler() { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email }); if (user.length === 0) { throw new Error("User does not exist"); } await Session.createNewSession(this.ctx, this.ctx, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); return { message: "Impersonation successful!" }; } } ``` ```tsx export default async function impersonate(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async (globalValidators) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin") ]) })(req, res, next); }, req, res ) let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email }); if (user.length === 0) { throw new Error("User does not exist"); } await superTokensNextWrapper( async (next) => { await createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); }, req, res ); res.json({ message: "Impersonation successful!" }); } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(req: NextRequest) { return withPreParsedRequestResponse(req, async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { let email = "..."; // read from request body let users = await SuperTokens.listUsersByAccountInfo("public", { email }); if (users.length === 0) { throw new Error("User does not exist"); } const session = await createNewSession(baseRequest, baseResponse, "public", users[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); return NextResponse.json({ message: "Impersonation successful" }); }); } ``` ```ts // @ts-ignore @Controller() export class ExampleController { // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. @Post("impersonate") @UseGuards(new AuthGuard({ overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => ([ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin") ]) })) async postLogin(@Req() req: Request, @Res() res: Response): Promise<{ message: string }> { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email }); if (user.length === 0) { throw new Error("User does not exist"); } await createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); return { message: "Impersonation successful!" }; } } ``` ```go // we are using emailpassword recipe here, but you can use the recipe you use // as well.. user, err := emailpassword.GetUserByEmail("public", email) if err != nil { // Send 500 to client return } if user == nil { // Send 400 to client cause user does not exist return } _, err = session.CreateNewSession(r, w, "public", user.ID, map[string]interface{}{ "isImpersonation": true, }, nil) if err != nil { err = supertokens.ErrorHandler(err, r, w) if err != nil { // Send 500 to client } return } // Send 200 success to client } ``` ```python from fastapi import Depends, Request from fastapi.responses import JSONResponse from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.asyncio import create_new_session from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.userroles import UserRoleClaim from supertokens_python.types.base import AccountInfoInput @app.post("/impersonate") # type: ignore async def impersonate( request: Request, session: SessionContainer = Depends( verify_session( # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")] ) ), ): email = "..." # get from request body # we use the email password recipe here, but you can use the recipe you use user = await list_users_by_account_info("public", AccountInfoInput(email=email)) if len(user) == 0: # return a 400 error to the client return await create_new_session( request, "public", user[0].login_methods[0].recipe_user_id, {"isImpersonation": True}, ) return JSONResponse({"message": "Impersonation complete!"}) ``` ```python from flask import jsonify from flask.wrappers import Request from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.session.syncio import create_new_session from supertokens_python.recipe.userroles import UserRoleClaim from supertokens_python.syncio import list_users_by_account_info from supertokens_python.types.base import AccountInfoInput @app.route("/impersonate", methods=["POST"]) # type: ignore @verify_session( # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")] ) def login(request: Request): email = "..." # get from request body # we use the email password recipe here, but you can use the recipe you use user = list_users_by_account_info("public", AccountInfoInput(email=email)) if len(user) == 0: # return a 400 error to the client return create_new_session( request, "public", user[0].login_methods[0].recipe_user_id, {"isImpersonation": True}, ) return jsonify({"message": "Impersonation complete!"}) ``` ```python from django.http import HttpRequest, JsonResponse from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.recipe.session.asyncio import create_new_session from supertokens_python.recipe.session.framework.django.asyncio import verify_session from supertokens_python.recipe.userroles import UserRoleClaim from supertokens_python.types.base import AccountInfoInput @verify_session( # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")] ) async def impersonate(request: HttpRequest): email = "..." # get from request body # we use the email password recipe here, but you can use the recipe you use user = await list_users_by_account_info("public", AccountInfoInput(email=email)) if len(user) == 0: # return a 400 error to the client return await create_new_session( request, "public", user[0].login_methods[0].recipe_user_id, {"isImpersonation": True}, ) return JsonResponse({"message": "User logged in!"}) ``` :::info Multi Tenancy Notice that the `"public"` `tenantId` goes to the function call above. This is the default `tenantId` in SuperTokens. If you are using the multi-tenancy feature and want to login into a different tenant, you can replace `"public"` with the `tenantId` you want to login into. You can fetch this `tenantId` based on the admin user's tenant (from their session), or you can pass it in the request body. ::: - The API should call from your frontend application such that the frontend SDKs' network interceptors are running. - In the API above, session verification runs first to ensure that the user has the admin role. If not, the API returns a `403` to the frontend. - The target user is then fetched based on their email ID. If the user does not exist, an error occurs (which you can map to a `400` status code). - A new session is then created using the target user's user ID. The `isImpersonation` flag goes as `true` in the access token payload to detect this on the frontend and show a message to the admin user that they are impersonating the target user (this is UI you would have to build if you want to). You can also use this custom access token payload to protect certain APIs which the admin cannot call, even if in impersonation mode. In the code, you use `isImpersonation`, but you can use anything else you like. In fact, you can even add the admin user's user ID to the access token payload (with a key like `adminUserId`), for logging purposes. - The new session tokens attach to the response and overwrite the existing admin session. Cookies apply if the request contains the `st-auth-mode: "cookie"` header, otherwise the mode is header-based auth. Since you would be calling this API via the frontend interceptors, you do not need to explicitly set this header since the frontend SDK does this on its own. - Once impersonated, the admin user can logout of the target user's session by clicking on the sign out button of your app - nothing special needs to happen there. # Post Authentication - Session Management - Advanced Workflows - Customize error handling Source: https://supertokens.com/docs/post-authentication/session-management/advanced-workflows/customize-error-handling ## Overview The following page shows the errors that the SuperTokens Session recipe throws and how you can customize them. --- ## Unauthorised error The system generates the error when someone accesses a protected backend API without a session. The default behavior is to clear session tokens (if any) and send a 401 to the frontend. ```tsx SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ //highlight-start errorHandlers: { onUnauthorised: async (message, request, response, userContext) => { // TODO: Write your own logic and then send a 401 response to the frontend }, } //highlight-end }) ] }); ``` ```go SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ //highlight-start errorHandlers: { onInvalidClaim: async (validatorErrors, request, response, userContext) => { // TODO: Write your own logic and then send a 403 response to the frontend }, } //highlight-end }) ] }); ``` ```go SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ //highlight-start errorHandlers: { onTokenTheftDetected: async (sessionHandle, userId, req, res, userContext) => { // TODO: Write your own logic and then send a 401 response to the frontend }, } //highlight-end }) ] }); ``` ```go SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ //highlight-start errorHandlers: { onTryRefreshToken: async (message, request, response, userContext) => { // TODO: Write your own logic and then send a 401 response to the frontend }, } //highlight-end }) ] }); ``` ```go The default behavior is to send a 200 to the frontend. ```tsx SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ //highlight-start errorHandlers: { onClearDuplicateSessionCookies: async (message, request, response, userContext) => { // TODO: Write your own logic and then send a 200 response to the frontend }, } //highlight-end }) ] }); ``` ```go # Post Authentication - Session Management - Advanced Workflows - Work with multiple API endpoints Source: https://supertokens.com/docs/post-authentication/session-management/advanced-workflows/multiple-api-endpoints ## Overview To enable use of sessions for multiple API endpoints, you need to update the configuration on both the frontend and backend. ## Before you start - All your API endpoints must have the same top level domain. For example, they can be `{"api.example.com", "api2.example.com"}`, but they cannot be `{"api.example.com", "api.otherdomain.com"}`. - Perform the backend configuration steps only if you are using cookie-based authentication. If using header based auth, please skip to step 3. ## Steps ### 1. Set the cookie domain in the backend configuration :::caution no-title This step is only applicable for cookie based authentication. ::: Set the `cookieDomain` value to be the common top level domain. For example, if your API endpoints are `{"api.example.com", "api2.example.com", "api3.example.com"}`, the common portion of these endpoints is `".example.com"` (The dot is important). You would need to set the following: ```tsx SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ // highlight-next-line cookieDomain: ".example.com", }) ] }); ``` ```go SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ // highlight-next-line cookieDomain: ".example.com", // highlight-next-line olderCookieDomain: "" // Set to an empty string if your previous cookieDomain was unset. Otherwise, use your old cookieDomain value. }) ] }); ``` ```go - Keep the value set for `olderCookieDomain` for 1 year because the cookie lifetime of the access token on the frontend is 1 year (even though the JWT expiry is a few hours). - If you have changed the `cookieDomain` more than once within one year, to prevent a stuck state, switch to [header-based auth](https://supertokens.com/docs/session/common-customizations/sessions/token-transfer-method#backend-configuration-optional) for all your clients. The important thing here is that you have to set the backend configuration to header even though that doc says it's optional. This ensures that all clients use header-based auth. - Changing the `cookieDomain` can cause a temporary spike in requests, even if you set the `olderCookieDomain` correctly. This happens because older sessions, with older cookie domain, require additional refresh calls to clear their old cookies and set new ones. This spike is a one-time event and should not recur after the update. ::: :::info Set the `olderCookieDomain` value to prevent clients from having multiple session cookies from different domains. This can happen when cookies from a previous domain are still valid and sent with requests. For instance, if your previous `cookieDomain` was `api.example.com` and the new one is `.example.com`, both sets of cookies would send to the `apiDomain` `api.example.com`, leading to an inconsistent state. This can cause issues until you clear the older cookies. Setting `olderCookieDomain` in the configuration ensures that the SuperTokens SDK can automatically remove these older cookies. ::: ### 3. Update the frontend configuration Set the same value for `sessionTokenBackendDomain` on the frontend. This allows the frontend SDK to apply interception and automatic refreshing across all your API calls: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ // highlight-next-line sessionTokenBackendDomain: ".example.com" }) ] }); ``` You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUISession.init({ // highlight-next-line sessionTokenBackendDomain: ".example.com" }) ] }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ sessionTokenBackendDomain: ".example.com" }), ], }); ``` ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ sessionTokenBackendDomain: ".example.com" }), ], }); ``` ```tsx supertokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ supertokensSession.init({ sessionTokenBackendDomain: ".example.com", }) ], }); ``` ```tsx SuperTokens.init({ apiDomain: "...", sessionTokenBackendDomain: ".example.com" }); ``` ```kotlin void initialiseSuperTokens() { SuperTokens.init( apiDomain: "...", sessionTokenBackendDomain: ".example.com", ); } ``` # Post Authentication - Session Management - Advanced Workflows - Disable frontend network interceptors Source: https://supertokens.com/docs/post-authentication/session-management/advanced-workflows/disable-frontend-interceptors ## Overview SuperTokens frontend SDKs add interceptors to networking libraries to: - Enable auto refreshing of session tokens. - Auto adding of the right request headers (Authorization header in case of header based auth, or the anti-csrf headers in case of cookie based auth). - Setting `credentials: true` for cookie based auth to ensure the browser adds session cookies. Whilst this helps for greenfield projects, for existing projects, you may want to disable this interception for your API calls. Take control of how you want to attach session tokens to the request yourself. You can do this as follows: ## Steps ### 1. Update the frontend configuration ```tsx Session.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("/auth")) { return false; } } catch (ignored) { } return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); } } } } }) ``` You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUISession.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("/auth")) { return false; } } catch (ignored) { } return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); } } } } }) ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx Session.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("/auth")) { return false; } } catch (ignored) { } return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); } } } } }) ``` ```tsx Session.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("/auth")) { return false; } } catch (ignored) { } return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); } } } } }) ``` ```tsx supertokensSession.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("/auth")) { return false; } } catch (ignored) { } return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); } } } } }) ``` You can use the `doesSessionExist` function to check if a session exists. ```tsx SuperTokens.init({ apiDomain: "...", override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("/auth")) { return false; } } catch (ignored) { } return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); } } } } }) ``` :::note At the moment this feature is not supported through the Android SDK. ::: :::note At the moment this feature is not supported through the iOS SDK. ::: :::note At the moment this feature is not supported through the Flutter SDK. ::: In the code above, the `shouldDoInterceptionBasedOnUrl` function is overridden to only allow interception for all API calls that start with `/auth` in their path. This ensures that API calls made from frontend SDKs (like sign out) continue to use the session tokens as expected by backend APIs. It also allows you to take control of how you want to attach session tokens to your own API calls (ones that have a path that don't start with `/auth`). If you want to also change how session tokens attach to API calls (like sign out), you can return `false` from the function override. Then, attach custom session headers using the [pre-API hook function](/docs/references/frontend-sdks/hooks#pre-api-hook) on the frontend. # Post Authentication - Session Management - Advanced Workflows - Usage inside an iframe Source: https://supertokens.com/docs/post-authentication/session-management/advanced-workflows/in-iframe ## Overview If your website can embed in an iframe that other websites consume, update your configuration based on this guide. ## Before you start If the sites where your iframe can embed share the same top-level domain as the iframe domain, then you can ignore this section. ## Steps ### 1. Update the frontend configuration - Set `isInIframe` to `true` during `Session.init` on the frontend. - You need to use `https` during testing / `dev` for this to work. You can use tools like [ngrok](https://ngrok.com/) to create a `dev` `env` with `https` on your website / API domain. - Switch to using header based auth - Provide a custom `windowHandler` and a custom `cookieHandler` to ensure that the app works on safari and chrome incognito. These handlers switch from using `document.cookies` to `localstorage` to store tokens on the frontend (since safari doesn't allow access to `document.cookies` in iframes), and use in-memory storage for chrome incognito (since chrome incognito doesn't even allow access to `localstorage`). You can find implementations of these handlers [here (`windowHandler`)](https://github.com/SuperTokens/supertokens-auth-react/blob/master/examples/with-next-iframe/config/windowHandler.js) and [here (`cookieHandler`)](https://github.com/SuperTokens/supertokens-auth-react/blob/master/examples/with-next-iframe/config/cookieHandler.js). ```tsx declare let cookieHandler: any // REMOVE_FROM_OUTPUT declare let windowHandler: any // REMOVE_FROM_OUTPUT SuperTokens.init({ cookieHandler, windowHandler, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ // highlight-start tokenTransferMethod: "header", isInIframe: true // highlight-end }) ] }); ``` You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx declare let cookieHandler: any // REMOVE_FROM_OUTPUT declare let windowHandler: any // REMOVE_FROM_OUTPUT supertokensUIInit({ cookieHandler, windowHandler, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUISession.init({ // highlight-start tokenTransferMethod: "header", isInIframe: true // highlight-end }) ] }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx declare let cookieHandler: any // REMOVE_FROM_OUTPUT declare let windowHandler: any // REMOVE_FROM_OUTPUT SuperTokens.init({ cookieHandler, windowHandler, appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ // highlight-start tokenTransferMethod: "header", isInIframe: true // highlight-end }) ], }) ``` ```tsx declare let cookieHandler: any // REMOVE_FROM_OUTPUT declare let windowHandler: any // REMOVE_FROM_OUTPUT SuperTokens.init({ cookieHandler, windowHandler, appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ // highlight-start tokenTransferMethod: "header", isInIframe: true // highlight-end }) ], }) ``` ```tsx declare let cookieHandler: any // REMOVE_FROM_OUTPUT declare let windowHandler: any // REMOVE_FROM_OUTPUT supertokens.init({ cookieHandler, windowHandler, appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ supertokensSession.init({ // highlight-start tokenTransferMethod: "header", isInIframe: true // highlight-end }) ], }) ``` :::caution Not applicable to mobile apps ::: :::caution Because of the restrictions on access to storage on Chrome incognito, you must use in-memory storage to store the tokens on the frontend. This in turn implies that if the user refreshes the page, or if your app does a full page navigation, the user logs out. ::: # Post Authentication - Session Management - Advanced Workflows - Blacklist access tokens Source: https://supertokens.com/docs/post-authentication/session-management/advanced-workflows/access-token-blacklisting ## Overview By default, session verification is stateless. This means that SuperTokens does not check that the session actually exists in the database, and only verifies the session by checking its signature. Whilst this makes session verifications fast, it also means that if you revoke a session, the user can still use it until the access token expires. If you want session verifications to fail immediately after revoking the session, you should force the session to check against the database. Since you can use this feature on a per API basis, we recommend that you only use it for non-GET APIs since only those are state changing. ## Before you start :::caution For managed service users, please check [the rate limit policy](/docs/deployment/rate-limits) before implementing this feature. If you suspect that you might breach the free limit you can: - [Email support](mailto:support@supertokens.com) to increase your rate limit. - Use the `checkDatabase` flag only on certain important APIs. For example, omit using it in any `GET` API as those are not state changing. - Implement your own method for keeping track of revoked access tokens by using a cache like Redis. ::: --- ## Using `Verify Session` ```tsx let app = express(); // highlight-start app.post("/like-comment", verifySession({ checkDatabase: true }), (req: SessionRequest, res) => { let userId = req.session!.getUserId(); // highlight-end //.... }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/like-comment", method: "post", //highlight-start options: { pre: [ { method: verifySession({ checkDatabase: true }) }, ], }, handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //highlight-end //... } }) ``` ```tsx let fastify = Fastify(); //highlight-start fastify.post("/like-comment", { preHandler: verifySession({ checkDatabase: true }), }, (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //highlight-end //.... }); ``` ```tsx async function likeComment(awsEvent: SessionEventV2) { let userId = awsEvent.session!.getUserId(); //.... }; //highlight-next-line exports.handler = verifySession(likeComment, { checkDatabase: true }); ``` ```tsx let router = new KoaRouter(); //highlight-start router.post("/like-comment", verifySession({ checkDatabase: true }), (ctx: SessionContext, next) => { let userId = ctx.session!.getUserId(); //highlight-end //.... }); ``` ```tsx class LikeComment { //highlight-start constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/like-comment") @intercept(verifySession({ checkDatabase: true })) @response(200) handler() { let userId = (this.ctx as SessionContext).session!.getUserId(); //highlight-end //.... } } ``` ```tsx // highlight-start export default async function likeComment(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ checkDatabase: true })(req, res, next); }, req, res ) let userId = req.session!.getUserId(); // highlight-end //.... } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } let userId = session!.getUserId(); //.... return NextResponse.json({}) }, { checkDatabase: true }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { @Post('example') @UseGuards(new AuthGuard({ checkDatabase: true })) // For more information about this guard please read our NestJS guide. async postExample(@Session() session: SessionContainer): Promise { //highlight-start let userId = session.getUserId(); //highlight-end //.... return true; } } ``` ```go let app = express(); app.post("/like-comment", async (req, res, next) => { try { let session = await Session.getSession(req, res, { checkDatabase: true }) if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... } catch (err) { next(err); } }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/like-comment", method: "post", handler: async (req, res) => { let session = await Session.getSession(req, res, { checkDatabase: true }) if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //... } }) ``` ```tsx let fastify = Fastify(); fastify.post("/like-comment", async (req, res) => { let session = await Session.getSession(req, res, { checkDatabase: true }) if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... }); ``` ```tsx async function likeComment(awsEvent: SessionEvent) { let session = await Session.getSession(awsEvent, awsEvent, { checkDatabase: true }) if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... }; //highlight-next-line exports.handler = middleware(likeComment); ``` ```tsx let router = new KoaRouter(); router.post("/like-comment", async (ctx, next) => { let session = await Session.getSession(ctx, ctx, { checkDatabase: true }) if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... }); ``` ```tsx class LikeComment { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/like-comment") @response(200) async handler() { let session = await Session.getSession(this.ctx, this.ctx, { checkDatabase: true }) if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... } } ``` ```tsx export default async function likeComment(req: SessionRequest, res: any) { let session = await superTokensNextWrapper( async (next) => { return await Session.getSession(req, res, { checkDatabase: true }); }, req, res ) if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } let userId = session!.getUserId(); //.... return NextResponse.json({}) }, { checkDatabase: true }); } ``` ```tsx @Controller() export class ExampleController { @Post('example') async postExample(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise { //highlight-start // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res, { checkDatabase: true }) if (session !== undefined) { const userId = session.getUserId(); } else { // user is not logged in... } //highlight-end //.... return true; } } ``` ```go # Post Authentication - Session Management - Session security Source: https://supertokens.com/docs/post-authentication/session-management/security ## Overview The following page takes you through some common security considerations that the **SuperTokens** `Session` recipe handles. --- ## Anti-csrf CSRF attacks can happen if a logged in user visits a malicious website which makes an API call to your website's API to maliciously change that user's data. To protect against this attack, the cookie `sameSite` attribute works along with some anti-csrf measures. This attribute declares if your cookies should restrict to a first-party or same-site context. Configuring `sameSite` can prevent CSRF attacks. For example, if `sameSite` is `lax`, the browser only sends cookies for requests that originate from the same top level domain as the API's domain. If a user visits a malicious site, requests from those sites do not have the session cookies. ### Configure anti-csrf :::caution - SuperTokens automatically defends against CSRF attacks. - Please only change this setting if you know what you are doing. If you are unsure, please feel free to [ask questions](https://supertokens.com/discord). - This setting does not apply while using header-based authentication, since they get the same protection as `antiCsrf` set to `VIA_CUSTOM_HEADER`. ::: You can change the `antiCsrf` configuration option to take control of the kind of protection you get. You can use on of the following values: - `"NONE"` would disable any anti-csrf protection from our end. You can use this if you have an implementation of CSRF protection. - `"VIA_CUSTOM_HEADER"` uses [this method](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers) to prevent CSRF protection. This sets automatically if `sameSite` is `none` or if your `apiDomain` and `websiteDomain` do not share the same top level domain name. - `"VIA_TOKEN"` uses an explicit anti-csrf token. Use this method if you want to allow any origin to query your APIs. This method may cause issues in browsers like Safari, especially if your site embeds as an `iframe`. ```tsx SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ // highlight-next-line antiCsrf: "VIA_CUSTOM_HEADER", // Should be one of "NONE" or "VIA_CUSTOM_HEADER" or "VIA_TOKEN" }) ] }); ``` ```go The ``sameSite`` cookie attribute declares if your cookies should restrict to a first-party or same-site context. The ``sameSite`` attribute can have three possible values: - ``none`` - Cookies attach in all contexts, that is, cookies attach to both first-party and cross-origin requests. - On Safari however, if third-party cookies do not work (which is the default behavior), and the website and `API` domains do not share the same top-level domain, then cookies do not go. Please check [the session management page](/docs/post-authentication/session-management/switch-between-cookies-and-header-authentication) to see how you can switch to using headers. - ``lax`` - Cookies are only sent in a first-party context and along with `GET` requests initiated by third party websites (that result in browser navigation - user clicking on a link). - ``strict`` - Cookies are only sent in a first-party context and not sent along with requests initiated by third party websites. ### Configuration :::caution - SuperTokens automatically sets the value of the ``sameSite`` cookie attribute based on your website and `API` domain configuration. - Please only change this setting if you are a web security expert. If you are unsure, please feel free to [ask questions](https://supertokens.com/discord). ::: ```tsx SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ // highlight-next-line cookieSameSite: "strict", // Should be one of "strict" or "lax" or "none" }), ], }); ``` ```go SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ //highlight-next-line cookieSecure: true, }) ] }); ``` ```go SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ //highlight-next-line useDynamicAccessTokenSigningKey: false, }) ] }); ``` :::caution Updating this value causes a spike in the session refresh API, as and when users visit your application. ::: ```go # Post Authentication - User Management - User management actions Source: https://supertokens.com/docs/post-authentication/user-management/common-actions ## Overview **SuperTokens** exposes a set of functions and APIs that you can use to have manual control over your users. Actions like fetching users or deleting them are available through different SDK calls. --- ## Get user ### By email ```tsx async function getUserInfo() { let usersInfo = await supertokens.listUsersByAccountInfo("public", { email: "test@example.com" }); /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ } ``` ```go async function handler() { let usersInfo = await supertokens.listUsersByAccountInfo("public", { phoneNumber: "+1234567890" }); /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ } ``` ```go let app = express(); app.get("/get-user-info", verifySession(), async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); // highlight-next-line let userInfo = await supertokens.getUser(userId) /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ }) ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/get-user-info", method: "get", options: { pre: [ { method: verifySession() }, ], }, // @ts-ignore handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); // highlight-next-line let userInfo = await supertokens.getUser(userId) /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ } }) ``` ```tsx const fastify = Fastify(); fastify.post("/like-comment", { preHandler: verifySession(), }, async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); // highlight-next-line let userInfo = await supertokens.getUser(userId) /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ }); ``` ```tsx async function getUserInfo(awsEvent: SessionEvent) { let userId = awsEvent.session!.getUserId(); // highlight-next-line let userInfo = await supertokens.getUser(userId) /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ }; exports.handler = verifySession(getUserInfo); ``` ```tsx let router = new KoaRouter(); router.get("/get-user-info", verifySession(), async (ctx: SessionContext, next) => { let userId = ctx.session!.getUserId(); // highlight-next-line let userInfo = await supertokens.getUser(userId) /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ }); ``` ```tsx class GetUserInfo { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @get("/get-user-info") @intercept(verifySession()) @response(200) async handler() { let userId = ((this.ctx as any).session as Session.SessionContainer).getUserId(); // highlight-next-line let userInfo = await supertokens.getUser(userId) /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ } } ``` ```tsx export default async function likeComment(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res ) let userId = req.session!.getUserId(); // highlight-next-line let userInfo = await supertokens.getUser(userId) /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } const userId = session!.getUserId(); // highlight-next-line let userInfo = await SuperTokens.getUser(userId) /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ return NextResponse.json({}); }); } ``` ```tsx // @ts-ignore // @ts-ignore @Controller() export class ExampleController { @Post('example') @UseGuards(new AuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Request() req: SessionRequest, @Session() session: Session, @Response({passthrough: true}) res: Response): Promise { let userId = session.getUserId(); // highlight-next-line let userInfo = await supertokens.getUser(userId) /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ return true; } } ``` ```go async function deleteUserForId() { let userId = "..." // get the user ID await deleteUser(userId); // this will succeed even if the userId didn't exist. } ``` ```go async function getUsers() { // get the latest 100 users let usersResponse = await getUsersNewestFirst({ tenantId: "public" }); let users = usersResponse.users; let nextPaginationToken = usersResponse.nextPaginationToken; // get the next 200 users usersResponse = await getUsersNewestFirst({ tenantId: "public", limit: 200, paginationToken: nextPaginationToken, }) users = usersResponse.users; nextPaginationToken = usersResponse.nextPaginationToken; // get for specific recipes usersResponse = await getUsersNewestFirst({ tenantId: "public", limit: 200, paginationToken: nextPaginationToken, // only get for those users who signed up with ^{recipeNameCapitalLetters} includeRecipeIds: ["^{rid}"], }) users = usersResponse.users; nextPaginationToken = usersResponse.nextPaginationToken; } ``` ```go async function getUsers() { // get the latest 100 users let usersResponse = await getUsersOldestFirst({ tenantId: "public" }); let users = usersResponse.users; let nextPaginationToken = usersResponse.nextPaginationToken; // get the next oldest 200 users usersResponse = await getUsersOldestFirst({ tenantId: "public", limit: 200, paginationToken: nextPaginationToken, }); users = usersResponse.users; nextPaginationToken = usersResponse.nextPaginationToken; // get for specific recipes usersResponse = await getUsersOldestFirst({ tenantId: "public", limit: 200, paginationToken: nextPaginationToken, // only get for those users who signed up with ^{recipeNameCapitalLetters} includeRecipeIds: ["^{rid}"] }); users = usersResponse.users; nextPaginationToken = usersResponse.nextPaginationToken; } ``` - If the `nextPaginationToken` is `undefined`, then there are no more users to loop through. - If there are no users in your app, then `nextPaginationToken` is `undefined` and `users` is an empty array - Each element in the `users` array is according to the output of the core API as shown in the [API documentation](https://app.swaggerhub.com/apis/supertokens/CDI/2.8.0#/Core/getUsers). ```go async function getCount() { let count = await getUserCount() } ``` ```go # Post Authentication - User Management - Allow users to change their data Source: https://supertokens.com/docs/post-authentication/user-management/allow-users-to-update-their-data ## Overview This guide shows you how to implement a feature that allows users to update their email or password. ## Before you start :::caution SuperTokens does not provide the UI for this type of use case. You need to create the UI and set up a route on your backend to have this functionality. ::: --- ## Email update This section has instructions on how to create a route, on your backend, to update a user's email. Calling this route checks if the new email is valid and not already in use and proceeds to update the user's account with the new email. ### Without email verification In this flow, a user can update their account's email without verifying the new email ID. #### 1. Create the email update endpoint - You need to create a route on the backend protected by the session verification middleware, ensuring that only an authenticated user can access the protected route. - To learn more about how to use the session verification middleware for other frameworks, click [this link](/docs/additional-verification/session-verification/protect-api-routes) ```tsx let app = express(); // highlight-start app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => { // TODO: see next steps }) // highlight-end ``` ```go let app = express(); app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => { // highlight-start let session = req.session!; let email = req.body.email; // Validate the input email if (!isValidEmail(email)) { // TODO: handle invalid email error return } // Update the email let resp = await Passwordless.updateUser({ recipeUserId: session.getRecipeUserId(), email: email }) if (resp.status === "OK") { // TODO: send successfully updated email response return } if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { // TODO: handle error that email exists with another account. return } if (resp.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { // This is possible if you have enabled account linking. // See our docs for account linking to know more about this. // TODO: tell the user to contact support. } throw new Error("Should never come here"); // highlight-end }) function isValidEmail(email: string) { let regexp = new RegExp( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ ); return regexp.test(email); } ``` ```go if err != nil { return false } return emailCheck } ``` ```python from re import fullmatch from flask import Flask, g, request from supertokens_python.recipe.passwordless.interfaces import ( EmailChangeNotAllowedError, UpdateUserEmailAlreadyExistsError, UpdateUserOkResult, ) from supertokens_python.recipe.passwordless.syncio import update_user from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session app = Flask(__name__) @app.route("/change-email", methods=["POST"]) # type: ignore @verify_session() def change_email(): # highlight-start session: SessionContainer = g.supertokens request_body = request.get_json() email = str(request_body["email"]) # type: ignore if request_body is None: # TODO: handle invalid body error return # validate the input email if not is_valid_email(email): # TODO: handle invalid email error return # update the users email update_response = update_user(session.get_recipe_user_id(), email=email) if isinstance(update_response, UpdateUserOkResult): # TODO send successful email update response return if isinstance(update_response, UpdateUserEmailAlreadyExistsError): # TODO handle error, email already exists return if isinstance(update_response, EmailChangeNotAllowedError): # This is possible if you have enabled account linking. # See our docs for account linking to know more about this. # TODO: tell the user to contact support. return raise Exception("Should never reach here") # highlight-end def is_valid_email(value: str) -> bool: return ( fullmatch( r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', value, ) is not None ) ``` ### With email verification In this flow, the user's account updates once they have verified the new email. #### 1. Create the email update endpoint - You need to create a route on the backend protected by the session verification middleware, ensuring that only an authenticated user can access the protected route. - To learn more about how to use the session verification middleware for other frameworks, click [this link](/docs/additional-verification/session-verification/protect-api-routes) ```tsx let app = express(); // highlight-start app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => { // TODO: see next steps }) // highlight-end ``` ```go let app = express(); app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => { // highlight-start let session = req.session!; let email = req.body.email; // validate the input email if (!isValidEmail(email)) { return res.status(400).send("Email is invalid"); } // Then, we check if the email is verified for this user ID or not. // It is important to understand that SuperTokens stores email verification // status based on the user ID AND the email, and not just the email. let isVerified = await EmailVerification.isEmailVerified(session.getRecipeUserId(), email); if (!isVerified) { if (!(await isEmailChangeAllowed(session.getRecipeUserId(), email, false))) { // this can come here if you have enabled the account linking feature, and // if there is a security risk in changing this user's email. return res.status(400).send("Email change not allowed. Please contact support"); } // Before sending a verification email, we check if the email is already // being used by another user. If it is, we throw an error. let user = (await supertokens.getUser(session.getUserId()))!; for (let i = 0; i < user?.tenantIds.length; i++) { // Since once user can be shared across many tenants, we need to check if // the email already exists in any of the tenants. let usersWithEmail = await supertokens.listUsersByAccountInfo(user?.tenantIds[i], { email }) for (let y = 0; y < usersWithEmail.length; y++) { if (usersWithEmail[y].id !== session.getUserId()) { // TODO handle error, email already exists with another user. return } } } // Now we create and send the email verification link to the user for the new email. await EmailVerification.sendEmailVerificationEmail(session.getTenantId(), session.getUserId(), session.getRecipeUserId(), email); // TODO send successful response that email verification email sent. return } // Since the email is verified, we try and do an update let resp = await Passwordless.updateUser({ recipeUserId: session.getRecipeUserId(), email: email, }); if (resp.status === "OK") { // TODO send successful response that email updated. return } if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { // TODO handle error, email already exists with another user. return } throw new Error("Should never come here"); // highlight-end }) function isValidEmail(email: string) { let regexp = new RegExp( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ ); return regexp.test(email); } ``` ```go if err != nil { return false } return emailCheck } ``` ```python from re import fullmatch from flask import Flask, g, request from supertokens_python.recipe.accountlinking.syncio import is_email_change_allowed from supertokens_python.recipe.emailverification.syncio import ( is_email_verified, send_email_verification_email, ) from supertokens_python.recipe.passwordless.interfaces import ( UpdateUserEmailAlreadyExistsError, UpdateUserOkResult, ) from supertokens_python.recipe.passwordless.syncio import update_user from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.syncio import get_user, list_users_by_account_info from supertokens_python.types.base import AccountInfoInput app = Flask(__name__) @app.route("/change-email", methods=["POST"]) # type: ignore @verify_session() def change_email(): # highlight-start session: SessionContainer = g.supertokens # type: ignore request_body = request.get_json() if request_body is None: # TODO: handle invalid body error return # validate the input email if not is_valid_email(request_body["email"]): # TODO: handle invalid email error return user_id = session.get_user_id() # Then, we check if the email is verified for this user ID or not. # It is important to understand that SuperTokens stores email verification # status based on the user ID AND the email, and not just the email. is_verified = is_email_verified(session.get_recipe_user_id(), request_body["email"]) if not is_verified: if not is_email_change_allowed( session.get_recipe_user_id(), request_body["email"], False ): # Email change is not allowed, send a 400 error return {"error": "Email change not allowed"}, 400 # Before sending a verification email, we check if the email is already # being used by another user. If it is, we throw an error. user = get_user(user_id) if user is not None: for tenant_id in user.tenant_ids: users_with_same_email = list_users_by_account_info( tenant_id, AccountInfoInput(email=request_body["email"]) ) for curr_user in users_with_same_email: # Since one user can be shared across many tenants, we need to check if # the email already exists in any of the tenants that belongs to this user. if curr_user.id != user_id: # TODO handle error, email already exists with another user. return # Create and send the email verification link to the user for the new email. send_email_verification_email( session.get_tenant_id(), user_id, session.get_recipe_user_id(), request_body["email"], ) # TODO send successful email verification response return # update the users email update_response = update_user( session.get_recipe_user_id(), email=request_body["email"] ) if isinstance(update_response, UpdateUserOkResult): # TODO send successful email update response return if isinstance(update_response, UpdateUserEmailAlreadyExistsError): # TODO handle error, email already exists return # highlight-end raise Exception("Should never reach here") def is_valid_email(value: str) -> bool: return ( fullmatch( r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', value, ) is not None ) ``` :::info Multi Tenancy - Notice that the process loops through all the tenants that this user belongs to check that for each of the tenants, there is no other user with the new email. If this step is not done, then calling `updateEmailOrPassword` would fail because the email is already used by another user in one of the tenants that this user belongs to. In that case, the verification process should not proceed either. - The `tenantId` of the current session is also passed when calling the `sendEmailVerificationEmail` function, ensuring that the link generated opens the tenant's UI that the user interacts with. - When calling `updateEmailOrPassword`, it returns `EMAIL_ALREADY_EXISTS_ERROR` if the new email exists in any of the tenants that the user ID is a part of. ::: #### 3. Update the account on successful email verification - Update the accounts email on successful email verification. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Passwordless.init({ flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", contactMethod: "EMAIL_OR_PHONE" }), EmailVerification.init({ mode: "REQUIRED", override: { apis: (oI) => { return { ...oI, verifyEmailPOST: async function (input) { // highlight-start let response = await oI.verifyEmailPOST!(input); if (response.status === "OK") { // This will update the email of the user to the one // that was just marked as verified by the token. await Passwordless.updateUser({ recipeUserId: response.user.recipeUserId, email: response.user.email, }); } return response; // highlight-end }, }; }, }, }), Session.init(), ], }); ``` ```go let app = express(); // highlight-start app.post("/change-password", verifySession(), async (req: SessionRequest, res: express.Response) => { // TODO: see next steps }) // highlight-end ``` ```go let app = express(); app.post("/change-password", verifySession(), async (req: SessionRequest, res: express.Response) => { // highlight-start // get the supertokens session object from the req let session = req.session // retrieve the old password from the request body let oldPassword = req.body.oldPassword // retrieve the new password from the request body let updatedPassword = req.body.newPassword // get the signed in user's email from the getUserById function let userInfo = await supertokens.getUser(session!.getUserId()) if (userInfo === undefined) { throw new Error("Should never come here") } let loginMethod = userInfo.loginMethods.find((lM) => lM.recipeUserId.getAsString() === session!.getRecipeUserId().getAsString() && lM.recipeId === "emailpassword"); if (loginMethod === undefined) { throw new Error("Should never come here") } const email = loginMethod.email!; // call signin to check that input password is correct let isPasswordValid = await EmailPassword.verifyCredentials(session!.getTenantId(), email, oldPassword) if (isPasswordValid.status !== "OK") { // TODO: handle incorrect password error return } // update the user's password using updateEmailOrPassword let response = await EmailPassword.updateEmailOrPassword({ recipeUserId: session!.getRecipeUserId(), password: updatedPassword, tenantIdForPasswordPolicy: session!.getTenantId() }) if (response.status === "PASSWORD_POLICY_VIOLATED_ERROR") { // TODO: handle incorrect password error return } // TODO: send successful password update response // highlight-end }) ``` ```go var requestBody RequestBody err := json.NewDecoder(r.Body).Decode(&requestBody) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // get the userId from the session userID := sessionContainer.GetUserID() // get the signed in user's email from the getUserById function userInfo, err := emailpassword.GetUserByID(userID) if err != nil { // TODO: Handle error return } // call signin to check that the input is correct isPasswordValid, err := emailpassword.SignIn(sessionContainer.GetTenantId(), userInfo.Email, requestBody.OldPassword) if err != nil { // TODO: Handle error return } // highlight-start if isPasswordValid.WrongCredentialsError != nil { // TODO: Handle error return } tenantId := sessionContainer.GetTenantId() updateResponse, err := emailpassword.UpdateEmailOrPassword(userID, &userInfo.Email, &requestBody.NewPassword, nil, &tenantId, nil) if err != nil { // TODO: Handle error return } if updateResponse.PasswordPolicyViolatedError != nil { // This error is returned if the new password doesn't match the defined password policy // TODO: Handle error return } // TODO: send successful password update response // highlight-end } ``` ```python from flask import g, request from supertokens_python.recipe.emailpassword.interfaces import ( PasswordPolicyViolationError, WrongCredentialsError, ) from supertokens_python.recipe.emailpassword.syncio import ( update_email_or_password, verify_credentials, ) from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.syncio import get_user @app.route("/change-password", methods=["POST"]) # type: ignore @verify_session() def change_password(): # highlight-start session: SessionContainer = g.supertokens # type: ignore # get the signed in user's email from the getUserById function users_info = get_user(session.get_user_id()) if users_info is None: raise Exception("TODO: Handle error. User not found.") # Find the login method for the current user login_method = next( ( lm for lm in users_info.login_methods if lm.recipe_user_id.get_as_string() == session.get_recipe_user_id().get_as_string() and lm.recipe_id == "emailpassword" ), None, ) if login_method is None: raise Exception("Should never come here") email = login_method.email if email is None: raise Exception("Email not found for the user") request_body = request.get_json() if request_body is None: # TODO: handle invalid body error return # call signin to check that the input password is correct isPasswordValid = verify_credentials( "public", email, password=request_body["oldPassword"] ) if isinstance(isPasswordValid, WrongCredentialsError): # TODO: handle incorrect password error return # update the users password update_response = update_email_or_password( session.get_recipe_user_id(), password=request_body["newPassword"], tenant_id_for_password_policy=session.get_tenant_id(), ) if isinstance(update_response, PasswordPolicyViolationError): # TODO: handle password policy violation error return # TODO: send successful password update response # highlight-end ``` :::info Multi Tenancy Notice that the `tenantId` passes as an argument to the `signIn` and the `updateEmailOrPassword` functions. This ensures that the current tenant has email password enabled, and ensures that the user's new password matches the password policy defined for their tenant (if different password policies exist for different tenants). If this user shares access across multiple tenants, their password changes for all tenants. ::: ### 3. Revoke all sessions associated with the user {{optional}} - Revoking all sessions associated with the user forces them to re-authenticate with their new password. ```tsx // the following example uses express let app = express(); app.post("/change-password", verifySession(), async (req: SessionRequest, res: express.Response) => { let userId = req.session!.getUserId(); /** * * ... * see previous step * ... * * */ // highlight-start // revoke all sessions for the user await Session.revokeAllSessionsForUser(userId) // revoke the current user's session, this removes the auth cookies, logging out the user on the frontend. await req.session!.revokeSession() //highlight-end // TODO: send successful password update response }) ``` ```go user_id = session.get_user_id() # TODO: see previous step... # highlight-start # revoke all sessions for the user revoke_all_sessions_for_user(user_id) # revoke the user's current session, this removes the auth cookies, logging out the user on the frontend session.sync_revoke_session() # highlight-end # TODO: send successful password update response ``` # Post Authentication - User Management - User metadata Source: https://supertokens.com/docs/post-authentication/user-management/user-metadata ## Overview You can use the `UserMetadata` recipe to store your custom data about each user. This can be any arbitrary values that are JSON serializable. The following page shows you how to enable and use the feature. --- ## Enable the `UserMetadata` recipe ```tsx SuperTokens.init({ supertokens: { connectionURI: "..." }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ // Initialize other recipes as seen in the quick setup guide // highlight-next-line UserMetadata.init(), ] }); ``` ```go let app = express(); app.post("/updateinfo", verifySession(), async (req, res) => { const session = req.session; const userId = session.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); // highlight-end res.json({ message: "successfully updated user metadata" }); }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/updateinfo", method: "post", options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { const session = req.session; const userId = session!.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); // highlight-end return res.response({ message: "successfully updated user metadata" }).code(200); }, }); ``` ```tsx let fastify = Fastify(); fastify.post( "/updateinfo", { preHandler: verifySession(), }, async (req, res) => { const session = req.session; const userId = session.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); // highlight-end res.send({ message: "successfully updated user metadata" }); }, ); ``` ```tsx async function updateinfo(awsEvent: SessionEvent) { const session = awsEvent.session; const userId = session!.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); // highlight-end return { body: JSON.stringify({ message: "successfully updated user metadata" }), statusCode: 200, }; } exports.handler = verifySession(updateinfo); ``` ```tsx let router = new KoaRouter(); router.post("/updateinfo", verifySession(), async (ctx: SessionContext, next) => { const session = ctx.session; const userId = session!.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); // highlight-end ctx.body = { message: "successfully updated user metadata" }; }); ``` ```tsx class UpdateInfo { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) {} @post("/updateinfo") @intercept(verifySession()) @response(200) async handler() { const session = this.ctx.session; const userId = session!.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); // highlight-end return { message: "successfully updated user metadata" }; } } ``` ```tsx export default async function updateInfo(req: any, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res, ); const session = (req as SessionRequest).session; const userId = session!.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); // highlight-end res.json({ message: "successfully updated user metadata" }); } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } const userId = session!.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); // highlight-end return NextResponse.json({ success: "successfully updated user metadata" }); }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. @Post("example") @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise<{ message: string }> { const userId = session.getUserId(); //highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); //highlight-end return { message: "successfully updated user metadata" }; } } ``` ```go let app = express(); app.post("/updateinfo", verifySession(), async (req, res) => { const session = req.session; const userId = session.getUserId(); // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); // highlight-end res.json({ preferences: metadata.preferences }); }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/updateinfo", method: "post", options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { const session = req.session; const userId = session!.getUserId(); // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); // highlight-end return res.response({ preferences: metadata.preferences }).code(200); }, }); ``` ```tsx let fastify = Fastify(); fastify.post( "/updateinfo", { preHandler: verifySession(), }, async (req, res) => { const session = req.session; const userId = session.getUserId(); // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); // highlight-end res.send({ preferences: metadata.preferences }); }, ); ``` ```tsx async function updateinfo(awsEvent: SessionEvent) { const session = awsEvent.session; const userId = session!.getUserId(); // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); // highlight-end return { body: JSON.stringify({ preferences: metadata.preferences }), statusCode: 200, }; } exports.handler = verifySession(updateinfo); ``` ```tsx let router = new KoaRouter(); router.post("/updateinfo", verifySession(), async (ctx: SessionContext, next) => { const session = ctx.session; const userId = session!.getUserId(); // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); // highlight-end ctx.body = { preferences: metadata.preferences }; }); ``` ```tsx class UpdateInfo { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) {} @post("/updateinfo") @intercept(verifySession()) @response(200) async handler() { const session = this.ctx.session; const userId = session!.getUserId(); // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); // highlight-end return { preferences: metadata.preferences }; } } ``` ```tsx export default async function updateInfo(req: any, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res, ); const session = (req as SessionRequest).session; const userId = session!.getUserId(); // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); // highlight-end res.json({ preferences: metadata.preferences }); } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } const userId = session!.getUserId(); // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); // highlight-end return NextResponse.json({ preferences: metadata.preferences }); }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. @Post("example") @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise<{ preferences: any }> { const userId = session.getUserId(); //highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); //highlight-end return { preferences: metadata.preferences }; } } ``` ```go ### Delete specific fields You can do this by calling the update metadata function and setting the field you want to remove to be `null`. For example, if you have the following metadata object for a user: ```json { "preferences": { "theme": "dark" }, "notifications": { "email": true }, "todos": ["use-text-notifs"] } ``` And you want to remove the `"notifications"` field, you can update the metadata object with the following JSON: ```json { "notifications": null } ``` This would result in the final metadata object: ```json { "preferences": { "theme": "dark" }, "todos": ["use-text-notifs"] } ``` :::info Important You can only remove the root level fields in the metadata object in this way. From the above example, if you set `preferences.theme: null`, then it does not remove the `"theme"` field, but instead sets it to a JSON null value. ::: In code, it would look like: ```tsx let app = express(); app.post("/updateinfo", verifySession(), async (req, res) => { const session = req.session; const userId = session.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); // highlight-end res.json({ message: "successfully updated user metadata" }); }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/updateinfo", method: "post", options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { const session = req.session; const userId = session!.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); // highlight-end return res.response({ message: "successfully updated user metadata" }).code(200); }, }); ``` ```tsx let fastify = Fastify(); fastify.post( "/updateinfo", { preHandler: verifySession(), }, async (req, res) => { const session = req.session; const userId = session.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); // highlight-end res.send({ message: "successfully updated user metadata" }); }, ); ``` ```tsx async function updateinfo(awsEvent: SessionEvent) { const session = awsEvent.session; const userId = session!.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); // highlight-end return { body: JSON.stringify({ message: "successfully updated user metadata" }), statusCode: 200, }; } exports.handler = verifySession(updateinfo); ``` ```tsx let router = new KoaRouter(); router.post("/updateinfo", verifySession(), async (ctx: SessionContext, next) => { const session = ctx.session; const userId = session!.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); // highlight-end ctx.body = { message: "successfully updated user metadata" }; }); ``` ```tsx class UpdateInfo { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) {} @post("/updateinfo") @intercept(verifySession()) @response(200) async handler() { const session = this.ctx.session; const userId = session!.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); // highlight-end return { message: "successfully updated user metadata" }; } } ``` ```tsx export default async function updateInfo(req: any, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res, ); const session = (req as SessionRequest).session; const userId = session!.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); // highlight-end res.json({ message: "successfully updated user metadata" }); } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } const userId = session!.getUserId(); // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); // highlight-end return NextResponse.json({ message: "successfully updated user metadata" }); }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. @Post("example") @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise<{ message: string }> { const userId = session.getUserId(); //highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); //highlight-end return { message: "successfully updated user metadata" }; } } ``` ```go let app = express(); app.post("/updateinfo", verifySession(), async (req, res) => { const session = req.session; const userId = session!.getUserId(); // highlight-start await UserMetadata.clearUserMetadata(userId); // highlight-end res.json({ success: true }); }); ``` ```tsx let server = Hapi.server({ port: 8000 }); server.route({ path: "/updateinfo", method: "post", options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { const session = req.session; const userId = session!.getUserId(); // highlight-start await UserMetadata.clearUserMetadata(userId); // highlight-end return res.response({ success: true }).code(200); }, }); ``` ```tsx let fastify = Fastify(); fastify.post( "/updateinfo", { preHandler: verifySession(), }, async (req, res) => { const session = req.session; const userId = session!.getUserId(); // highlight-start await UserMetadata.clearUserMetadata(userId); // highlight-end res.send({ success: true }); }, ); ``` ```tsx async function updateinfo(awsEvent: SessionEvent) { const session = awsEvent.session; const userId = session!.getUserId(); // highlight-start await UserMetadata.clearUserMetadata(userId); // highlight-end return { body: JSON.stringify({ success: true }), statusCode: 200, }; } exports.handler = verifySession(updateinfo); ``` ```tsx let router = new KoaRouter(); router.post("/updateinfo", verifySession(), async (ctx: SessionContext, next) => { const session = ctx.session; const userId = session!.getUserId(); // highlight-start await UserMetadata.clearUserMetadata(userId); // highlight-end ctx.body = { success: true }; }); ``` ```tsx class UpdateInfo { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) {} @post("/updateinfo") @intercept(verifySession()) @response(200) async handler() { const session = this.ctx.session; const userId = session!.getUserId(); // highlight-start await UserMetadata.clearUserMetadata(userId); // highlight-end return { success: true }; } } ``` ```tsx export default async function updateInfo(req: any, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res, ); const session = (req as SessionRequest).session; const userId = session!.getUserId(); // highlight-start await UserMetadata.clearUserMetadata(userId); // highlight-end res.json({ success: true }); } ``` ```tsx // @ts-ignore SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } const userId = session!.getUserId(); // highlight-start await UserMetadata.clearUserMetadata(userId); // highlight-end return NextResponse.json({ success: true }); }); } ``` ```tsx // @ts-ignore @Controller() export class ExampleController { @Post("example") @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise<{ success: boolean }> { const userId = session.getUserId(); //highlight-start // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. await UserMetadata.clearUserMetadata(userId); //highlight-end return { success: true }; } } ``` ```go # Post Authentication - User Management - Account deduplication Source: https://supertokens.com/docs/post-authentication/user-management/account-deduplication ## Overview Users may forget the initial method they used to sign up and may create multiple accounts with the same email ID - leading to a poor user experience. Preventing this from happening refers to account deduplication. As an example, assume that your app has Google and GitHub login. There exists a user who had signed up with Google using their email ID - `user@gmail.com`. If this user then tries to sign up with GitHub, which has this same email (`user@gmail.com`), your app disallows this. It shows them an appropriate message like "Your account already exists via Google sign in. Please use that instead." ### Comparison to account linking Related to this problem is also the concept of account linking. The difference is that whilst deduplication prevents duplicate sign ups, account linking allows duplicate sign ups, but implicitly merges the duplicate accounts into one. ## Steps ### 1. Override the authentication recipes The approach to implementing account deduplication is to override the backend functions / APIs. This way, you can check if a user already exists and return an error to the frontend if the condition is true. ```tsx let recipeList = [ Passwordless.init({ contactMethod: "EMAIL", // REMOVE_FROM_OUTPUT flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // REMOVE_FROM_OUTPUT override: { apis: (originalImplementation) => { return { ...originalImplementation, createCodePOST: async function (input) { if ("email" in input) { let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { email: input.email }); if (existingUsers.length === 0) { // this means this email is new so we allow sign up return originalImplementation.createCodePOST!(input); } if (existingUsers.find(u => u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== undefined)) { // this means that the existing user is a passwordless login user. So we allow it return originalImplementation.createCodePOST!(input); } return { status: "GENERAL_ERROR", message: "Seems like you already have an account with another method. Please use that instead." } } // phone number based login, so we allow it. return originalImplementation.createCodePOST!(input); }, } } } }), ThirdParty.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, signInUp: async function (input) { let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { email: input.email }); if (existingUsers.length === 0) { // this means this email is new so we allow sign up return originalImplementation.signInUp(input); } if (existingUsers.find(u => u.loginMethods.find(lM => lM.hasSameThirdPartyInfoAs({ id: input.thirdPartyId, userId: input.thirdPartyUserId }) && lM.recipeId === "thirdparty") !== undefined)) { // this means we are trying to sign in with the same social login. So we allow it return originalImplementation.signInUp(input); } // this means that the email already exists with another social or passwordless login method, so we throw an error. throw new Error("Cannot sign up as email already exists"); } } }, apis: (originalImplementation) => { return { ...originalImplementation, signInUpPOST: async function (input) { try { return await originalImplementation.signInUpPOST!(input); } catch (err: any) { if (err.message === "Cannot sign up as email already exists") { // this error was thrown from our function override above. // so we send a useful message to the user return { status: "GENERAL_ERROR", message: "Seems like you already have an account with another method. Please use that instead." } } throw err; } } } } } }) ] ``` ```go // so we send a useful message to the user return tpmodels.SignInUpPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "Seems like you already have an account with another method. Please use that instead.", }, }, nil } return resp, err } return originalImplementation }, }, }), } } ``` ```python from typing import Any, Dict, Optional, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.recipe import passwordless, thirdparty from supertokens_python.recipe.passwordless.interfaces import ( APIInterface as PasswordlessAPIInterface, ) from supertokens_python.recipe.passwordless.interfaces import ( APIOptions as PasswordlessAPIOptions, ) from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.thirdparty.interfaces import ( APIInterface as ThirdPartyAPIInterface, ) from supertokens_python.recipe.thirdparty.interfaces import ( APIOptions as ThirdPartyAPIOptions, ) from supertokens_python.recipe.thirdparty.interfaces import ( RecipeInterface, ) from supertokens_python.recipe.thirdparty.provider import Provider, RedirectUriInfo from supertokens_python.recipe.thirdparty.types import ( RawUserInfoFromProvider, ThirdPartyInfo, ) from supertokens_python.types import GeneralErrorResponse from supertokens_python.types.base import AccountInfoInput def override_thirdparty_functions(original_implementation: RecipeInterface): original_sign_in_up = original_implementation.sign_in_up async def sign_in_up( third_party_id: str, third_party_user_id: str, email: str, is_verified: bool, oauth_tokens: Dict[str, Any], raw_user_info_from_provider: RawUserInfoFromProvider, session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, user_context: Dict[str, Any], ): existing_users = await list_users_by_account_info( tenant_id, AccountInfoInput(email=email) ) if len(existing_users) == 0: # this means this email is new so we allow sign up return await original_sign_in_up( third_party_id, third_party_user_id, email, is_verified, oauth_tokens, raw_user_info_from_provider, session, should_try_linking_with_session_user, tenant_id, user_context, ) if any( any( lm.recipe_id == "thirdparty" and lm.has_same_third_party_info_as( ThirdPartyInfo(third_party_user_id, third_party_id) ) for lm in user.login_methods ) for user in existing_users ): # this means we are trying to sign in with the same social login. So we allow it return await original_sign_in_up( third_party_id, third_party_user_id, email, is_verified, oauth_tokens, raw_user_info_from_provider, session, should_try_linking_with_session_user, tenant_id, user_context, ) # this means that the email already exists with another social login method. # so we throw an error. raise Exception("Cannot sign up as email already exists") original_implementation.sign_in_up = sign_in_up return original_implementation def override_thirdparty_apis(original_implementation: ThirdPartyAPIInterface): original_sign_in_up_post = original_implementation.sign_in_up_post async def sign_in_up_post( provider: Provider, redirect_uri_info: Optional[RedirectUriInfo], oauth_tokens: Optional[Dict[str, Any]], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: ThirdPartyAPIOptions, user_context: Dict[str, Any], ): try: return await original_sign_in_up_post( provider, redirect_uri_info, oauth_tokens, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) except Exception as e: if str(e) == "Cannot sign up as email already exists": return GeneralErrorResponse( "Seems like you already have an account with another social login provider. Please use that instead." ) raise e original_implementation.sign_in_up_post = sign_in_up_post return original_implementation def override_passwordless_apis(original_implementation: PasswordlessAPIInterface): original_create_code_post = original_implementation.create_code_post async def create_code_post( email: Union[str, None], phone_number: Union[str, None], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: PasswordlessAPIOptions, user_context: Dict[str, Any], ): if email is not None: existing_users = await list_users_by_account_info( tenant_id, AccountInfoInput(email=email) ) if len(existing_users) == 0: # this means this email is new so we allow sign up return await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) if any( user.login_methods and any( lm.recipe_id == "passwordless" and lm.has_same_email_as(email) for lm in user.login_methods ) for user in existing_users ): # this means that the existing user is a passwordless login user. So we allow it return await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) return GeneralErrorResponse( "Seems like you already have an account with another method. Please use that instead." ) # phone number based login, so we allow it. return await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) original_implementation.create_code_post = create_code_post return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ passwordless.init( contact_config=..., # type: ignore flow_type="...", # type: ignore override=passwordless.InputOverrideConfig( apis=override_passwordless_apis, ), ), thirdparty.init( override=thirdparty.InputOverrideConfig( apis=override_thirdparty_apis, functions=override_thirdparty_functions ) ), ], ) ``` In the above code snippet, override the `signInUpPOST` (third party recipe) and the `createCodePOST` (passwordless recipe) API as well as the `signInUp` recipe function. The frontend calls the `signInUpPOST` API after the user returns to the app from the third-party provider's login page. The API then exchanges the auth code with the provider and calls the `signInUp` function with the user's email and third-party info. The system calls the `createCodePOST` API when the user enters their email, or phone number during passwordless login. This API generates the passwordless OTP / link and sends it to the user's email / phone. The `signInUp` recipe function is overridden to: - Get all ThirdParty or Passwordless users that have the same input email. - If no users exist with that email, it means that this is a new email and the system calls the `originalImplementation` function to create a new user. - If instead, a user exists, but has the same `thirdPartyId` and `thirdPartyUserId`, implying that this is a sign in (for example a user who had signed up with Google is signing in with Google), the operation proceeds by calling the `originalImplementation` function. - If neither of the conditions above match, it means that the user is trying to sign up with a third party provider whilst they already have an account with another provider or via passwordless login. Here, the system throws an error with some custom message. Finally, the `signInUpPOST` API is overridden to catch that custom error and return a [general error status](/docs/references/backend-sdks/api-overrides#error-management) to the frontend with a message displayed to the user in the sign in form. The `createCodePOST` API is also overridden to perform similar checks: - If the input is phone number based, then the system calls the `originalImplementation` function allowing sign up or sign in. This is OK since social login is always email based, there is no scope of duplication. - Otherwise, get all ThirdParty or Passwordless users that have the same input email. - If no users exist with that email, it means that this is a new email and the system calls the `originalImplementation` function to create a new user. - Else, check if the existing user is not a Third Party login user, implying that it's a Passwordless login user. Here, the `originalImplementation` function is also called to allow the user to sign in. - If neither of the conditions above match, it means that the user is trying to sign up with passwordless login whilst they already have an account with a third party provider. Here, the system returns an appropriate message to display on the frontend. :::info Multi Tenancy For a multi tenant setup, the customisations above ensure that multiple accounts with the same email don't exist within a single tenant. To ensure no duplication across all tenants, when fetching the list of existing users, loop through all tenants in the app. You can fetch them by using the `listAllTenants` function of the multi tenancy recipe. ::: # Post Authentication - User Management - User banning Source: https://supertokens.com/docs/post-authentication/user-management/user-banning ## Overview This tutorial shows you how to add a user banning feature to your SuperTokens authentication flows. The guide makes use of the plugins functionality which provides the ability to ban/unban users. ## How it works The plugin makes use of the `UserRoles` feature to keep track of banned users. When you ban someone, they get assigned a new role, `banned`, and their session gets revoked immediately. Additionally, the default session validation logic gets overridden to prevent users with the banned role from accessing the application. ### Caching To avoid extra network calls during session verification, the plugin uses an in-memory cache to keep track of the ban status. The cache gets reloaded during the first session verification, after a server start, causing a slight increase in latency. If you are working with a serverless environment or with distributed applications, you can implement your own caching strategy through overrides. ## Before you start The user banning plugin supports only the `React` and `NodeJS` SDKs. Support for other platforms is under active development. Besides initializing the plugin, you also have to include the `UserRoles` recipe in your SuperTokens configuration. ## Steps ### 1. Initialize the backend plugin #### 1.1 Install the plugin ```bash npm install @supertokens-plugins/user-banning-nodejs ``` #### 1.2 Update your backend SDK configuration ```typescript SuperTokens.init({ appInfo: { // your app info }, recipeList: [ UserRoles.init(), // Required: UserRoles recipe must be initialized // your other recipes ], plugins: [ UserBanningPlugin.init({ userBanningPermission: "ban-user", // Optional: defaults to "ban-user" bannedUserRole: "banned", // Optional: defaults to "banned" }), ], }); ``` :::warning no-title Make sure to also initialize the `UserRoles` recipe if you haven't already. ::: ### 2. Initialize the frontend plugin #### 2.1 Install the plugin ```bash npm install @supertokens-plugins/user-banning-react ``` #### 2.2 Update your frontend SDK configuration ```typescript SuperTokens.init({ appInfo: { // your app info }, recipeList: [ // your recipes ], plugins: [ UserBanningPlugin.init({ userBanningPermission: "ban-user", // Should match backend config bannedUserRole: "banned", // Should match backend config onPermissionFailureRedirectPath: "/", // Optional: defaults to "/" }), ], }); ``` ### 3. Ban users #### 3.1 Using the user banning interface The plugin provides a complete administrative interface accessible at `/admin/ban-user`. Before you access the interface, make sure that your user has the required permission, `ban-user` by default. Read the [role management actions page](/docs/additional-verification/user-roles/role-management-actions#add-permissions) for instructions on how to add permissions to your users. User banning UI From the interface you can check the banning status of a user. Based on that status, you can either ban or remove the ban for that account. #### 3.2 Using direct API calls You can also manage user bans programmatically using the exposed API endpoints. ##### Ban/unban user ```javascript // Ban a user const banResponse = await fetch("/plugin/supertokens-plugin-user-banning/ban?tenantId=public", { method: "POST", credentials: "include", // Include session cookies headers: { "Content-Type": "application/json", }, body: JSON.stringify({ email: "user@example.com", // You can also pass the userId instead of the email // userId: "user123", isBanned: true, // true to ban, false to remove ban }), }); const banResult = await banResponse.json(); if (banResult.status === "OK") { console.log("User banned successfully"); } else { console.error("Failed to ban user:", banResult.message); } // Remove ban from a user const unbanResponse = await fetch("/plugin/supertokens-plugin-user-banning/ban?tenantId=public", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ email: "user@example.com", isBanned: false, }), }); ``` ##### Check ban status ```javascript // Check if a user is banned const statusResponse = await fetch( "/plugin/supertokens-plugin-user-banning/ban?tenantId=public&email=user@example.com", { method: "GET", credentials: "include", } ); const status = await statusResponse.json(); if (status.status === "OK") { console.log("User is banned:", status.banned); } else { console.error("Error checking ban status:", status.message); } ``` ## Customization ### Implement a custom user interface To create a custom user interface you can use the `usePluginContext` hook. It allows you to access the plugin's API methods and configuration in custom React components: ```typescript function MyCustomAdminComponent() { const { api, pluginConfig, t } = usePluginContext(); const handleBanUser = async (email: string) => { try { const result = await api.updateBanStatus("public", email, true); if (result.status === "OK") { console.log("User banned successfully"); } else { console.error("Failed to ban user:", result.message); } } catch (error) { console.error("Error:", error); } }; const handleCheckBanStatus = async (email: string) => { try { const result = await api.getBanStatus("public", email); if (result.status === "OK") { console.log("User has ban:", result.banned); } else { console.error("Error:", result.message); } } catch (error) { console.error("Error:", error); } }; return (

{t("PL_UB_BAN_PAGE_TITLE")}

); } ``` Besides user banning you can also look into other user management features and security measures: Attack Protection Suite Prevent suspicious authentication attempts. User Roles Implement role-based access control for your users. Plugins Reference General information on how plugins work. # Post Authentication - Account Linking - Important concepts Source: https://supertokens.com/docs/post-authentication/account-linking/important-concepts ## Overview The following page describes concepts that are relevant towards understanding how account linking works in **SuperTokens**. ## References Each authentication recipe has a unique *user* object. ### Primary and non-primary users The system identifies a primary or a non-primary user by the `isPrimaryUser` boolean within the user object. The primary user ID remains constant when accounts link to it. A user can become a primary user only if no other primary users share the same email, third-party information, or phone number across all tenants. This applies to all tenants to which the user belongs. Hence, two primary users with the same email address, one using email/password login and the other using social login. :::info Multi-tenancy Additionally, the following scenario is not permitted in a multi-tenant context: - User A is a primary user with email `test@example.com` and belongs to tenants `t1` and `t2`. - User B is a primary user with email `test@example.com` and belongs to tenant `t2`. This is not allowed because there are two primary users with the same email in tenant `t2`. ::: For accounts to link, one user must be a primary user. The resulting user ID of the linked accounts becomes the primary user's ID. For example, if User A is a primary user with user ID `u1` and links to User B (a non-primary user) with user ID `u2`, the resulting user's primary ID becomes `u1`. The recipe ID remains `u1` for User A and `u2` for User B. **Tenant Considerations:** * When designating a user as a primary user, the system verifies the primary user condition (as defined above) across all tenants to which the user belongs. * When linking two accounts, the primary user condition and account linking condition must satisfy across the union of all tenants to which the primary and non-primary users belong. For example, if User A (tenant `t1`, `t2`) links to User B (tenant `t3`), the system checks the conditions across `t1`, `t2`, and `t3`. ### Primary user ID and recipe user ID For most purposes, you care about the user's primary user ID. For example, when a user with two login methods, email password and social login, logs in, you get back the same primary user ID when you get their user ID from the session (or read the `sub` claim in the JWT). However, if you want to identify what the login method used for the current session is, you can use the session's `recipeUserId`. Then you can compare its value to the `recipeUserId` in each of the `loginMethods` in the user object. Some functions from the backend SDK also accept a `recipeUserId` as a parameter. For example, the `updateEmailOrPassword` function from the `emailpassword` recipe takes in a `recipeUserId` to determine which login method needs the update. If it took a `string` user ID instead, and you passed it a user's primary user ID, it may unintentionally lead to updating the wrong login method's email or password. It may also throw an error if the primary user is not an email password user. ### User unlinking User unlinking is the process of removing a login method from a user. For example, if a user has both email password and social login, and they want to remove their social login, you can use the unlinking function from the backend SDK. A few scenarios exist here: 1. If unlinking a login method that **is not** associated with the primary user, it results in two users: one as the primary user and the other as the non-primary user. For example, if User A (primary user, with email password login) links with User B (social login), and then you unlink User B, this results in two separate users: User A (primary user), with one login method (email password) and User B (non-primary user) with social login. The primary user ID of user B changes to be equal to their recipe user ID. 2. If you unlink a login method associated with the primary user, it deletes the login method of the primary user ID. For example, if User A (primary user, with email password login) links with User B (social login), and then you unlink User A, this results in the deletion of the email password user. Only User B remains, which is a social login user, and its primary user ID equals User A's primary user ID (even though the system deleted the login method for A). Any metadata, role, sessions info continues to exist. 3. If unlinking a User A which is a primary user ID, but it has not linked users, it results in this user becoming a non-primary user. :::important All the above checks happen automatically. You don't need to worry about them. But it is important to understand what's happening. ::: ## Security Below is the list of all points in time when account linking occurs, and for each point, you can see the list of security checks that happen: ### During sign up #### First case - Email is `e1` - Email verification: `false` (is the case with email password sign up or social login with a provider that does not require email verification) ##### Checks done: - If there is no primary user with the same email, then sign up is not allowed if there exists any other non-primary account with the same email and that account is not verified. This occurs because if not, there is a risk that if this user signs up and becomes a primary user, the other account (which could be malicious), might resend a verification email. The user might click on it (since they signed up) and verify the malicious account, thereby linking it to their account. This way, the malicious user gains access to the victim's account. - If there exists a primary user with the same email, then the system rejects the sign up of this new user. This occurs because if allowed, and this sign up is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This causes the new, malicious account to link, thereby giving the malicious user access to the victim's account. ###### What users see: - In case of email password login, users see an account already exists error. In this case, they can try logging in with another method, or go through the password reset flow, which creates a new email password account for them as well as verify it. - In case of social login, users see that they should try a different login method for security reasons. #### Second case - Email is `e1` - Email verified: `true` (this is the case with social login with a provider that requires email verification, like Google, or it could be a passwordless sign up) ##### Checks done: - Same as in the first case. - If there exists a primary user with the same email, then the system allows sign up only if there exists at least one login method in the primary user with email `e1` which the system has verified. This occurs because if not, then the following account takeover is possible: - Malicious user signs up with email password, with email `e2`, verifies it, and becomes a primary user. - They then change their email to `e1`, and keep it in an unverified state. - The actual user (victim) does a Google sign in with email `e1`. - If this sign up is not stopped, then the new sign up links to the primary user, and the malicious user gains access to the victim's account. ##### What users see: - Users see that they should try a different login method for security reasons. ### During sign in #### First case - Email is `e1` - Email verified: `false` - User is not a primary user ##### Checks done: - If there exists another user with the same email, and they are not a primary user, but their email remains unverified, the system disallows this sign in because if it allowed it, and this user verifies their email, it results in this user becoming a primary user. If the other account then sends an email verification email, this user may click on it (they may not get too suspicious), and verify the other account, thereby linking it to their account. This way, the other account, which may belong to a malicious user, gains access to the victim's account. - If there exists another user with the same email, and they are not a primary user, the system disallows signing in. This occurs because if allowed, and this sign in is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This causes the new, malicious account to link, thereby giving the malicious user access to the victim's account. ##### What users see: - For email password sign in, users see a wrong credentials error message. This prompts them to go through the password reset flow, which also marks the email as verified, thereby allowing them to sign in. This also blocks sign ins from malicious users, since the new password is only known to the actual owner of the email. - For passwordless or social login, users see that they should try a different login method for security reasons. #### Second case - The user first does a social login sign up with email `e1`. - They then change their email to `e2` on the provider and tries signing in again. - The third party provider does not verify emails, resulting in `e2` remaining unverified. ##### Checks done: - If the social login user is **not** a primary user, and there exists another primary user with email `e2`, then the system disallows sign in here. This occurs because if allowed, and this sign in is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This causes the new, malicious account to link, thereby giving the malicious user access to the victim's account. - If this user is a primary user, and there exists another primary user with email `e2`, the system disallows sign in because there can't be two primary users with the same email. The system rejects the email update which happens during sign in for social login. ##### What users see: - For the first point, users see a message asking them to try a different login method, or contact support for security reasons. - For the second case, users see that email update is not allowed and to contact support. ### During the password reset flow - Malicious user has email password and a social login account with email `e1`, and the system links them both. - They then change their email to `e2` for the email password login, which belongs to the victim. - Actual owner of `e2` tries to sign up, but sees that their account already exists (Case 1), they then try to sign in, but can't cause they don't know the password. They try the password reset flow. #### Checks done: - In this case, we deny generating the password reset token because if we did, then the real user would change the password of the email password account, and also mark it as verified. They would have access to the account, however, the malicious user could also then login using social login (with email `e1`) to access the same account. During password reset, the system does not generate a token if the email password account for that email associates with a primary account that also has other emails / phone numbers. If the email for which the password is being reset is not verified for any of the login methods in that primary user, the token is not generated. #### What users see: - They see a message telling them that the reset password link was not generated because of account takeover risk, and to contact support. ### During the email update flow: - A user has email `e1`, and they want to change it to email `e2` #### Checks done: - If the user's account is not a primary user, and there exists another primary user with email `e2`, then the system disallows email update here. This occurs because if allowed, and this email update is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This causes the system to link the new, malicious account, thereby giving the malicious user access to the victim's account. - If this user is a primary user, and there exists another primary user with email `e2`, the system disallows email update because there can't be two primary users with the same email. #### What users see: - If the email update is happening during sign in of social login, users see a message that email update is not allowed and to contact support. - If this is happening post login (from a settings page), then you can send any message you want to the user, since this would be your custom API. --- # Post Authentication - Account Linking - Automatic account linking Source: https://supertokens.com/docs/post-authentication/account-linking/automatic-account-linking ## Overview Automatic account linking is a feature that allows users to automatically sign in to their existing account using more than one login method. On a high level, SuperTokens automatically links the accounts for the different login methods provided that: - Their emails or phone numbers are the same. - They have verified their emails or phone numbers. SuperTokens ensures that accounts are automatically linked only if there is no risk of account takeover. ## Before you start ## Steps ### 1. Enable the recipe You can enable this feature by providing the following callback implementation on the backend SDK: ```tsx // Prevent account linking if the user already exists in your database function checkIfUserHasAssociatedInformation(accountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined): boolean { if(!accountInfo.recipeUserId || !user) return false; const userId = accountInfo.recipeUserId.getAsString(); const hasAssociatedInformation = false return hasAssociatedInformation; } supertokens.init({ supertokens: { connectionURI: "...", apiKey: "..." }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ // highlight-start AccountLinking.init({ shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { // This step is required if you are saving user information in your own database. const hasAssociatedInformation = checkIfUserHasAssociatedInformation(newAccountInfo, user); if (hasAssociatedInformation) { return { shouldAutomaticallyLink: false, } } return { shouldAutomaticallyLink: true, shouldRequireVerification: true } } }) // highlight-end ] }); ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import accountlinking from supertokens_python.types import User from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink from typing import Dict, Any, Optional, Union # Prevent account linking if the user already exists in your database async def check_if_user_has_associated_information(account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User]) -> bool: if not account_info.recipe_user_id or not user: return False _user_id = account_info.recipe_user_id.get_as_string() # Add your own implementation here has_associated_information = False return has_associated_information async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any] ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: has_associated_information = await check_if_user_has_associated_information(new_account_info, user) # This step is required if you are saving user information in your own database. if has_associated_information: return ShouldNotAutomaticallyLink() return ShouldAutomaticallyLink(should_require_verification=True) init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework='...', # type: ignore recipe_list=[ accountlinking.init(should_do_automatic_account_linking=should_do_automatic_account_linking) ], ) ``` ## Input | Argument | Type | Description | |----------|------|-------------| | `newAccountInfo` | `AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }` | Contains information about the user whose account is going to link or become a primary user. Includes email, social login info, phone number, and login method (`emailpassword`, `thirdparty`, or `passwordless`). May contain `recipeUserId` during account linking. Note: When `newAccountInfo.recipeUserId !== undefined && user !== undefined`, extra logic checks if the user ID has associated data in your application db to prevent data loss. | | `user` | `User \| undefined` | If not `undefined`, indicates `newAccountInfo` user links to this user. If `undefined`, `newAccountInfo` user becomes a primary user. | | `session` | `SessionContainerInterface \| undefined` | Session object of the user who is linking. `undefined` for first factor login. Defined if user completed first factor and calls sign up/in API again (MFA or social login linking). | | `tenant` | `string` | ID of the tenant the user is signing in/up to. | | `userContext` | `any` | User defined context object. | ## Output | Argument | Type | Description | |----------|------|-------------| | `shouldAutomaticallyLink` | `boolean` | If `true`, `newAccountInfo` links or becomes primary user during API call (subject to security checks). If `false`, no account linking operation occurs. | | `shouldRequireVerification` | `boolean` | If `true`, account linking only happens if `newAccountInfo` verifies. **Strongly recommended to keep as `true` for security.** |
:::important If you are returning `shouldRequireVerification` as `true`, then you need to also [enable the email verification recipe](/docs/additional-verification/email-verification/initial-setup) in `REQUIRED` mode. This means that if the login method does not inherently verify the email (like for email password login), SuperTokens requires the user to go through the email verification flow first. Then, it attempts auto linking of the account. For other login methods like sign in with Google, the email is already verified during login. The user does not need to verify the email again, and account linking occurs immediately. If you enable email verification in `OPTIONAL` mode, the user can access the account after email password login. However, account linking only occurs after they verify their email later on. This is risky because while the user had access to their email password account after sign up, they could lose access after verification and account linking completes due to the change in the primary user ID. A callback is available to help migrate data from one user ID to another. ::: You can use the input of the function to dynamically decide if you want to do account linking for a particular user and / or login method or not. ## References ### Automatic account linking scenarios #### During sign up If there exists another account with the same email or phone number within the current tenant, the new account links to the existing account if: - The existing account is a primary user - If `shouldRequireVerification` is `true`, the new account needs creation via a method that has the email as verified (for example via passwordless or google login). If the new method doesn't inherently verify the email (like in email password login), the accounts link post email verification. - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. #### During sign in If the current user is not already linked and if there exists another user with the same email or phone number within the current tenant, the accounts link if: - The user signing into is not a primary user, and the other user with the same email / phone number is a primary user - If `shouldRequireVerification` is `true`, the current account (that's signing into) has its email as verified. - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. #### After email verification If the current user whose email got verified is not a primary user, and there exists another primary user in the same tenant with the same email, then the two accounts link if: - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. :::info no-title For a primary user, if two login methods (L1 & L2) share the same email, but L1's email verifies and L2's does not, SuperTokens automatically verifies L2's email under these conditions: - The user logs in with L2. - The `updateEmailOrPassword` (email password recipe) or `updateUser` (passwordless recipe) function calls to update L2's email to match L1's. ::: #### During the password reset flow If there already exists a user with the same email in a non email password recipe (social login for example), and the user is doing a password reset flow, a new email password user creates and links to the existing account if: - The non email password user is a primary user. - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. :::info Email update implications When updating a user's login email, SuperTokens ensures account linking conditions remain valid. A primary user's email cannot update to match another primary user's email. User A has login methods `AL1` (email `e1`) and `AL2` (email `e1`). User B has login methods `BL1` (email `e2`) and `BL2` (email `e3`). Updating `AL1`'s email to `e2` or `e3` is not allowed, as it would create two primary users with the same email. **Email updates occur in these scenarios:** * `updateEmailOrPassword` function (email password recipe) * `updateUser` function (passwordless recipe) * Social login (if email from provider has changed) If the update violates account linking rules, the operation fails: * Function calls return a status indicating the update was impossible. * Social login API calls return a status prompting the user to contact support. ::: ### User data changes during account linking When two accounts link, the primary user ID of the non primary user changes. For example, if User A has a primary user ID `p1` and user B, which is a non primary user, has a user ID of `p2`, and they link, then the primary user ID of User B changes to `p1`. This has an effect that if the user logs in with login method from User B, the `session.getUserId()` returns `p1`. If there was any older data associated with User B (against user ID `p2`), in your database, that data essentially becomes "lost". To prevent this scenario, you should: - Make sure that you return `false` for `shouldAutomaticallyLink` boolean in the `shouldDoAutomaticAccountLinking` function implementation if there exists a `recipeUserId` in the `newAccountInfo` object, and if you have some information related to that user ID in your own database. This appears in the [code snippet above](#enabling-automatic-account-linking). - If you do not want to return `false` in this case, and want the accounts to link, then make sure to implement the `onAccountLinked` callback:
```tsx supertokens.init({ supertokens: { connectionURI: "...", apiKey: "..." }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ AccountLinking.init({ shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { return { shouldAutomaticallyLink: true, shouldRequireVerification: true } }, // highlight-start onAccountLinked: async (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => { let olderUserId = newAccountInfo.recipeUserId.getAsString() let newUserId = user.id; // TODO: migrate data from olderUserId to newUserId in your database... } // highlight-end }) ] }); ``` :::caution If your logic in `onAccountLinked` throws an error, then it is not called again, and still results in linking the accounts. However, the end user would see an error on the UI as the API returns a `500` status code. They can retry the login action and log into the primary user's account as expected. ::: ### Error status codes The following is a list of error status codes that the end user might see during their interaction with the login UI (as a general error message in the pre-built UI). ## `ERR_CODE_001` - This can happen during creating a password reset code in the email password flow: - API path and method: `/user/password/reset/token POST` - Output JSON: ```json { "status": "PASSWORD_RESET_NOT_ALLOWED", "reason": "Reset password link was not created because of account take over risk. Please contact support. (ERR_CODE_001)" } ``` - The pre-built UI on the frontend displays this error in the following way: pre-built UI screenshot showing error message for ERR_CODE_001. - Below is the scenario for when this status returns: A malicious user, User A, which is a primary user, has login methods with email `e1` (social login) and email `e1` (`emailpassword` login). If user A changes their `emailpassword` email to `e2` (which is in unverified state), and the real user of `e2` (the victim) tries to sign up via email password, they see a message saying that the email already exists. The victim may then try to do a password reset (thinking they had previously signed up). If this happens, and the victim resets the password (since they are the real owner of the email), then they can login to the account, and the attacker can spy on what the user is doing via their third party login method. To prevent this scenario, enforcement ensures that the password link is only generated if the primary user has at least one login method that has the input email ID and verifies it, or if not, checks that the primary user has no other login method with a different email, or phone number. If these cases are not satisfied, then the system returns the error code `ERR_CODE_001`. - To resolve this, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. **You can do these actions using the user management dashboard.** ## `ERR_CODE_002` - This can happen during the passwordless recipe's create or consume code API (during sign up): - API path and method: `/signinup/code POST` or `/signinup/code/consume POST` - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_002)" } ``` - The pre-built UI on the frontend displays this error in the following way: pre-built UI screenshot showing error message for ERR_CODE_002. - Below is an example scenario for when this status returns (one amongst many): A user is trying to sign up using passwordless login method with email `e1`. There exists an email password login method with `e1`, which remains unverified (owned by an attacker). If this scenario occurs, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they signed up, they do not get suspicious), and then the attacker's login method links to the passwordless login method. This way, the attacker gains access to the user's account. To prevent this, sign up with passwordless login is not allowed in case there exists another account with the same email and remains unverified. - To resolve this issue, you should ask the user to try another login method (which already has their email), or then mark their email as verified in the other account that has the same email, before asking them to retry passwordless login. **You can do these actions using the user management dashboard.** ## `ERR_CODE_003` This used to be an error code which is no longer valid and you can ignore it. ## `ERR_CODE_004` - This can happen during the third party recipe's `/signinup` API (during sign in): - API path and method: `/signinup POST` - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_004)" } ``` - The pre-built UI on the frontend displays this error in the following way: Pre-built UI screenshot showing error for message ERR_CODE_004. - Below is an example scenario for when this status returns (one amongst many): There exists a `thirdparty` user with email `e1`, sign in with Google (owned by the victim, and the email is verified). There exists another `thirdparty` login method with email, `e2` (owned by an attacker), such as login with GitHub. The attacker then goes to their GitHub and changes their email to `e1` (which is in unverified state). The next time the attacker tries to login, via GitHub, they see this error code. Login is prevented, because if it wasn't, then the attacker might send an email verification link to `e1`, and if the victim clicks on it, then the attacker's account will link to the victim's account. - To resolve this issue, you can delete the login method that has the unverified email, or if manually mark the unverified account as verified (if you confirm the identity of its owner). **You can do these actions using the user management dashboard.** ## `ERR_CODE_005` - This can happen during the third party recipe's `signinup` API (during sign in): - API path and method: `/signinup POST` - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_005)" } ``` - The pre-built UI on the frontend displays this error in the following way: Pre-built UI screenshot showing error message for ERR_CODE_005. - Below is as example scenario for when this status returns (one amongst many): There exists a primary, third party user with email `e1`, sign in with Google. There exists another email password user with email `e2`, which is a primary user. If the user changes their email on Google to `e2`, and then try logging in via Google, they see this error code. This occurs because if it wasn't, then it would result in two primary users having the same email, which violates one of the account linking rules. - To resolve this issue, you can make one of the primary users as non primary (use the unlink button against the login method on the user management dashboard). Once the user is not a primary user, you can ask the user to re-login with that method, and it should auto link that account with the existing primary user. ## `ERR_CODE_006` - This can happen during the third party recipe's `signinup` API (during sign up): - API path and method: `/signinup POST` - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_006)" } ``` - The pre-built UI on the frontend displays this error in the following way: Pre-built UI screenshot showing error message for ERR_CODE_006. - Below is as example scenario for when this status returns (one amongst many): A user is trying to sign up using third party login method with email `e1`. There exists an email password login method with `e1`, which remains unverified (owned by an attacker). If the third party sign up is allowed, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they signed up, they do not get suspicious), and then the attacker's login method links to the third party login method. This way, the attacker has access to the user's account. To prevent this, sign up with third party login is not allowed in case there exists another account with the same email and remains unverified. - To resolve this issue, you should ask the user to try another login method (which already has their email), or then manually mark their email as verified in the other account that has the same email, before asking them to retry third party login. **You can do these actions using the user management dashboard.** ## `ERR_CODE_007` - This can happen during the email password sign up API: - API path and method: `/signup POST` - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)" } ``` - The pre-built UI on the frontend displays this error in the following way: Pre-built UI screenshot showing error message for ERR_CODE_007. - Below is as example scenario for when this status returns (one amongst many): There exists a primary, social login account with email `e1`, sign in with Google. If an attacker tries to sign up with email password with email `e1`, the system sends an email verification email to the victim, and they may click it since they had previously signed up with Google. This links the attacker's account to the victim's account. - To resolve this issue, you can ask the user to try and login, or go through the reset password flow. ## `ERR_CODE_008` - This can happen during the email password sign in API: - API path and method: `/signin POST` - Output JSON: ```json { "status": "SIGN_IN_NOT_ALLOWED", "reason": "Cannot sign in due to security reasons. Please try resetting your password, use a different login method or contact support. (ERR_CODE_008)" } ``` - The pre-built UI on the frontend displays this error in the following way: Pre-built UI screenshot showing error message for ERR_CODE_008. - Below is as example scenario for when this status returns (one amongst many): There exists a primary, social login account with email `e1`, sign in with Google. There also exists an email password account (owned by the attacker) that remains unverified with the same email `e1` (this is not a primary user). If the attacker tries to sign in with email password, they see this error. This occurs because if it wasn't, then the attacker might send an email verification email on sign in, and the actual user may click on it (since they had previously signed up). Upon verifying that account, the system links the attacker's account to the victim's account. - To resolve this issue, you can ask the user to try the reset password flow. ## `ERR_CODE_014` - This can happen when adding a password to an existing session user: - API Path is `/signup POST`. - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_014)" } ``` - An example scenario of when in the following scenario: - Let's say that the app configures to not have automatic account linking during the first factor. - A user creates an email password account with email `e1`, verifies it, and links social login account to it with email `e2`. - The user logs out, and then creates a social login account with email `e1`. Then, they receive a request to add a password to this account. Since an email password account with `e1` already exists, SuperTokens tries and links that to this new account, but fails, since the email password account with `e1` is already a primary user. - To resolve this, it is recommended to manually link the `e1` social login account with the `e1` email password account. Alternatively, enable automatic account linking for first factor to prevent the above scenario. ## `ERR_CODE_015` - This can happen when adding a password to an existing session user: - API Path is `/signup POST`. - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_015)" } ``` - An example scenario of when in the following scenario: - A user creates a social login account with email `e1` which becomes a primary user. - The user logs out, and creates another social login account with email `e2`, which also becomes a primary user. - The user receives a request to add a password for the new account with an option to also specify an email with it (this is strange, but theoretically possible). They enter the email `e1` for the email password account. - This causes this type of error since the linking of the new social login and email account fails since there already exists another primary user with the same (`e1`) email. - To resolve this, it is recommended not allowing users to specify an email when asking them to add a password for their account. ## `ERR_CODE_016` - This can happen when adding a password to an existing session user: - API Path is `/signup POST`. - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_016)" } ``` - An example scenario of when in the following scenario: - Let's say that the app is configured to not have automatic account linking during the first factor. - A user signs up with a social login account using Google with email `e1`, and they add another social account, with Facebook, with the same email. - The user logs out and creates another social login account with email `e1` (say GitHub), and then tries and adds a password to this account with email `e1`. Here, SuperTokens tries and makes the GitHub login a primary user, but fails, since the email `e1` is already a primary user (with Google login). - To resolve this, it is recommended that you manually link the `e1` GitHub social login account with the `e1` Google social login account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. ## `ERR_CODE_020` - This can happen during association of a third party login to an existing session's account. - API Path is `/signinup POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_020)" } ``` - This can happen when the third party account that is trying to link to the session's account is not verified. It could happen when you are trying to associate a social login account to a user, but that social account's email is not verified (and if the email of that account is not the same as the current session's account's email). - To resolve this, you can return `shouldRequireVerification` as `false` in the `shouldDoAutomaticAccountLinking` function implementation, or you can only allow users to link social login accounts that give verified accounts. ## `ERR_CODE_021` - This can happen during association of a third party login to an existing session's account. - API Path is `/signinup POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_021)" } ``` - This can happen when the third party account that is trying to link to the session's account is already linked with another primary user. ## `ERR_CODE_022` - This can happen during association of a third party login to an existing session's account. - API Path is `/signinup POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_022)" } ``` - This can happen when the third party account that is trying to link to the session's account has the same email as another primary user. ## `ERR_CODE_023` - This can happen during association of a third party login to an existing session's account. - API Path is `/signinup POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_023)" } ``` - To link the third party user with the session user, we need to make sure that the session user is a primary user. However, that can fail if there exists another primary user with the same email as the session user, and in this case, this error returns to the frontend. ## `ERR_CODE_024` - This happens during third party sign in, when the user is trying to sign in with a non-primary user, and the third party provider does not verify their email, and their exists a primary user with the same email. This can also happen the other way around wherein the user is trying to sign in with the primary user (unverified email), and there exists a non-primary user with the same email. - API Path is `/signinup POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_024)" } ``` - You can resolve this by deleting the (non primary) user that has the same email ID, or by manually marking the email of the user as verified for the login method that they are trying to sign in with. #### Changing the error message on the frontend If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](/docs/references/frontend-sdks/prebuilt-ui/translations). --- # Post Authentication - Account Linking - Manual account linking Source: https://supertokens.com/docs/post-authentication/account-linking/manual-account-linking ## Overview Manual account linking allows you to take control of when and which accounts link. With this, you can implement flows like: - Connecting social login accounts to an existing account post login. - Adding a password to an account that a social or passwordless login created. - Linking accounts which don't have the same email or phone number, or have a different identifier altogether. ## Before you start ## Steps ### 1. Initialize the account linking recipe ```tsx supertokens.init({ supertokens: { connectionURI: "...", apiKey: "..." }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ // highlight-start AccountLinking.init() // highlight-end ] }); ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import accountlinking init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework='...', # type: ignore recipe_list=[ accountlinking.init() ], ) ``` In the above, SuperTokens does not automatically link accounts (during sign up or sign in APIs) by returning `shouldAutomaticallyLink: false`. Initializing the recipe is still important to use the functions from the SDK as shown below. It is of course possible to [enable auto account linking](./automatic-account-linking) and still use the functions for manual account linking below. ### 2. Create a primary user To link two accounts, you first need to make one of them a primary user: ```tsx async function makeUserPrimary(recipeUserId: RecipeUserId) { let response = await AccountLinking.createPrimaryUser(recipeUserId); if (response.status === "OK") { if (response.wasAlreadyAPrimaryUser) { // The input user was already a primary user and accounts can be linked to it. } else { // User is now primary and accounts can be linked to it. } let modifiedUser = response.user; console.log(modifiedUser.isPrimaryUser); // will print true } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { // This happens if there already exists another primary user with the same email or phone number // in at least one of the tenants that this user belongs to. } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR") { // This happens if this user is already linked to another primary user. } } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.accountlinking.asyncio import create_primary_user from supertokens_python.types import RecipeUserId async def make_user_primary(recipe_user_id: RecipeUserId): response = await create_primary_user(recipe_user_id) if response.status == "OK": if response.was_already_a_primary_user: # The input user was already a primary user and accounts can be linked to it. pass else: # User is now primary and accounts can be linked to it. pass modified_user = response.user print(modified_user.is_primary_user) # will print True elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if there already exists another primary user with the same email or phone number # in at least one of the tenants that this user belongs to. pass elif response.status == "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR": # This happens if this user is already linked to another primary user. pass ``` ```python from supertokens_python.recipe.accountlinking.syncio import create_primary_user from supertokens_python.types import RecipeUserId def make_user_primary(recipe_user_id: RecipeUserId): response = create_primary_user(recipe_user_id) if response.status == "OK": if response.was_already_a_primary_user: # The input user was already a primary user and accounts can be linked to it. pass else: # User is now primary and accounts can be linked to it. pass modified_user = response.user print(modified_user.is_primary_user) # will print True elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if there already exists another primary user with the same email or phone number # in at least one of the tenants that this user belongs to. pass elif response.status == "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR": # This happens if this user is already linked to another primary user. pass ``` ### 3. Link accounts Once a user has become a primary user, you can link other accounts to this user: ```tsx // we are linking the input recipeUserId to the primaryUserId async function linkAccounts(primaryUserId: string, recipeUserId: RecipeUserId) { let response = await AccountLinking.linkAccounts(recipeUserId, primaryUserId); if (response.status === "OK") { if (response.accountsAlreadyLinked) { // The input users were already linked } else { // The two users are now linked } let modifiedUser = response.user; console.log(modifiedUser.loginMethods); // this will now contain the login method of the recipeUserId as well. } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { // This happens if there already exists another primary user with the same email or phone number // as the recipeUserId's account. } else if (response.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER") { // This happens if the input primaryUserId is not actually a primary user ID. // You can call createPrimaryUserId and call linkAccountsAgain } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { // This happens if the input recipe user ID is already linked to another primary user. // You can call unlink accounts on the recipe user ID and then try linking again. } } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.accountlinking.asyncio import link_accounts from supertokens_python.types import RecipeUserId async def link_accounts_helper(primary_user_id: str, recipe_user_id: RecipeUserId): response = await link_accounts(recipe_user_id, primary_user_id) if response.status == "OK": if response.accounts_already_linked: # The input users were already linked pass else: # The two users are now linked pass modified_user = response.user print(modified_user.login_methods) # this will now contain the login method of the recipeUserId as well. elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if there already exists another primary user with the same email or phone number # as the recipeUserId's account. pass elif response.status == "INPUT_USER_IS_NOT_A_PRIMARY_USER": # This happens if the input primaryUserId is not actually a primary user ID. # You can call create_primary_user and call link_accounts again pass elif response.status == "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if the input recipe user ID is already linked to another primary user. # You can call unlink_accounts on the recipe user ID and then try linking again. pass ``` ```python from supertokens_python.recipe.accountlinking.syncio import link_accounts from supertokens_python.types import RecipeUserId def link_accounts_helper(primary_user_id: str, recipe_user_id: RecipeUserId): response = link_accounts(recipe_user_id, primary_user_id) if response.status == "OK": if response.accounts_already_linked: # The input users were already linked pass else: # The two users are now linked pass modified_user = response.user print(modified_user.login_methods) # this will now contain the login method of the recipeUserId as well. elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if there already exists another primary user with the same email or phone number # as the recipeUserId's account. pass elif response.status == "INPUT_USER_IS_NOT_A_PRIMARY_USER": # This happens if the input primaryUserId is not actually a primary user ID. # You can call create_primary_user and call link_accounts again pass elif response.status == "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if the input recipe user ID is already linked to another primary user. # You can call unlink_accounts on the recipe user ID and then try linking again. pass ``` ### 4. Unlink accounts If you want to unlink an account from its primary user ID, you can use the following function: ```tsx async function unlinkAccount(recipeUserId: RecipeUserId) { let response = await AccountLinking.unlinkAccount(recipeUserId); if (response.status === "OK") { if (response.wasLinked) { // This means that we unlinked the account from its primary user ID } else { // This means that the user was never linked in the first place } if (response.wasRecipeUserDeleted) { // This is true if we call unlinkAccount on the recipe user ID of the primary user ID user. // We delete this user because if we don't and we call getUserById() on this user's ID, SuperTokens // won't know which user info to return - the primary user, or the recipe user. // Note that even though the recipe user is deleted, the session, metadata, roles etc for this // primary user is still intact, and calling getUserById(primaryUserId) will still return // the user object with the other login methods. } else { // There not exists a user account which is not a primary user, with the recipeUserId = to the // input recipeUserId. } } } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.recipe.accountlinking.asyncio import unlink_account from supertokens_python.types import RecipeUserId async def unlink_account_helper(recipe_user_id: RecipeUserId): response = await unlink_account(recipe_user_id) if response.was_linked: # This means that we unlinked the account from its primary user ID pass else: # This means that the user was never linked in the first place pass if response.was_recipe_user_deleted: # This is true if we call unlink_account on the recipe user ID of the primary user ID user. # We delete this user because if we don't and we call get_user_by_id() on this user's ID, SuperTokens # won't know which user info to return - the primary user, or the recipe user. # Note that even though the recipe user is deleted, the session, metadata, roles etc for this # primary user is still intact, and calling get_user_by_id(primary_user_id) will still return # the user object with the other login methods. pass else: # There now exists a user account which is not a primary user, with the recipe_user_id equal to the # input recipe_user_id. pass ``` ```python from supertokens_python.recipe.accountlinking.syncio import unlink_account from supertokens_python.types import RecipeUserId def unlink_account_helper(recipe_user_id: RecipeUserId): response = unlink_account(recipe_user_id) if response.was_linked: # This means that we unlinked the account from its primary user ID pass else: # This means that the user was never linked in the first place pass if response.was_recipe_user_deleted: # This is true if we call unlink_account on the recipe user ID of the primary user ID user. # We delete this user because if we don't and we call get_user_by_id() on this user's ID, SuperTokens # won't know which user info to return - the primary user, or the recipe user. # Note that even though the recipe user is deleted, the session, metadata, roles etc for this # primary user is still intact, and calling get_user_by_id(primary_user_id) will still return # the user object with the other login methods. pass else: # There now exists a user account which is not a primary user, with the recipe_user_id equal to the # input recipe_user_id. pass ``` ### 5. Convert a `userId` into a `recipeUserId` If you notice, the input to a lot of the functions above is of type `RecipeUserId`. You can convert a string userID into a `RecipeUserId` in the following way: ```tsx async function getAsRecipeUserIdType(userId: string) { return SuperTokens.convertToRecipeUserId(userId); } ``` :::note At the moment this feature is not supported through the Go SDK. ::: ```python from supertokens_python.types import RecipeUserId user_id = "some_user_id"; recipe_user_id = RecipeUserId(user_id) ``` The reason for this type is that it prevents bugs wherein a function expects a recipe user ID (like `createNewSession`, or `updateEmailOrPassword` from email password recipe). However, you might pass in the primary user ID instead. ### 6. Other helper functions Our SDK also exposes other helper functions: - `AccountLinking.createPrimaryUserIdOrLinkAccounts`: Given a recipe user ID, this function attempts linking it with any primary user ID that has the same email or phone number associated with it. If no such primary user exists, this function makes the input user account a primary one. - `AccountLinking.getPrimaryUserThatCanBeLinkedToRecipeUserId`: Given a recipe user ID, this function returns a primary user ID which this user can link to, based on matching emails / phone numbers. If no such primary user exists, this function returns `undefined`. - `AccountLinking.canCreatePrimaryUser`: Given a recipe user ID, this function returns a status `OK` if the user can become a primary user, and a different status otherwise (indicating why it can't become a primary user). A user can become a primary user if there exists no other primary user with the same email or phone number across all the tenants that this user belongs to. - `AccountLinking.canLinkAccounts`: Given a `recipeUserId` and a primary user ID, this function returns a status `OK` if the accounts can link, and if not, it returns a different status (indicating why the accounts can't link). Accounts can link if the recipe user ID is not already linked to another primary user, and if the resulting primary user does not have any email / phone number in common with another primary user across all the tenants that it belongs to. - `AccountLinking.isSignUpAllowed`: Given the login info (email for example) of the new user, who is trying to sign up, this function returns `true` if it's safe to allow them to sign up, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#support-status-codes) to see why this might return `false`. - `AccountLinking.isSignInAllowed`: Given the login info (email for example) of a user, who is trying to sign in, this function returns `true` if it's safe to allow them to sign in, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#support-status-codes) to see why this might return `false`. - `AccountLinking.isEmailChangeAllowed`: Given the recipe user ID and the new email for update, this function returns `true` if it's safe to update the email, else `false`. Below are the conditions in which `false` returns: - If the input recipe user is a primary user, then ensure that the new email doesn't belong to any other primary user. If it does, the change is not allowed since multiple primary users can't have the same email. - If the recipe user is not a primary user, and if the new email is not verified, then check if there exists a primary user with the same email. If it exists, do not allow the email change. The disallowance occurs because if this email changes, and the system sends an email verification email, then the primary user may end up clicking on the link by mistake, causing account linking to happen which can result in account takeover if this recipe user is malicious. --- # Post Authentication - Account Linking - Link social accounts Source: https://supertokens.com/docs/post-authentication/account-linking/link-social-accounts ## Overview The following guide shows you how to link a social account to an existing user account. The idea here is to reuse the existing sign up APIs, but call them with a session's access token. The APIs then create a new recipe user for that login method based on the input, and then link that to the session user. Of course, there are security checks done to ensure there is no account takeover risk, and this guide goes through them as well. ## Before you start We do not provide pre-built UI for this flow since it's probably something you want to add in your settings page or during the sign up process. This guide focuses on which APIs to call from your own UI. The frontend code snippets below refer to the `supertokens-web-js` SDK. You can continue to use this even if you have initialised the `supertokens-auth-react` SDK, on the frontend. ## Steps ### 1. Enable account linking on the backend SDK ```tsx supertokens.init({ supertokens: { connectionURI: "...", apiKey: "..." }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ // highlight-start AccountLinking.init({ shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { if (user === undefined) { return { shouldAutomaticallyLink: true, shouldRequireVerification: true } } if (session !== undefined && session.getUserId() === user.id) { return { shouldAutomaticallyLink: true, shouldRequireVerification: true } } return { shouldAutomaticallyLink: false } } }) // highlight-end ] }); ``` :::note At the moment this feature is not supported through the Go SDK. ::: :::note At the moment this feature is not supported through the Go SDK. ::: In the above implementation of `shouldDoAutomaticAccountLinking`, account linking is only allowed if the input session is present. This means that the process is trying to link a social login account to an existing session user. Otherwise, account linking is not allowed, which means that first factor account linking does not occur. If you want to enable that too, you can see [the automatic account linking page](./automatic-account-linking). ### 2. Create a UI to show social login buttons and handle login First, you need to detect which social login methods are already linked to the user. You can do this by inspecting the [user object](/docs/references/backend-sdks/user-object) on the backend and checking the `thirdParty.id` property (the values are like `google`, `facebook` etc). Then you have to create your own UI which asks the user to pick a social login provider to connect to. Once they click on one, redirect them to that provider's page. After login, the provider redirects the user back to your application (on the same path as the first factor login). You then call the APIs to consume the OAuth tokens and link the user. The exact implementation of the above is available [in the initial setup documentation](/docs/authentication/social/initial-setup). The two big differences in the implementation are: - When you call the `signinup` API, you need to provide the session's access token in the request. If you are using the frontend SDK, the frontend network interceptors automatically handle this. The access token enables the backend to get a session and then link the social login account to session user. - New types of failure scenarios exist when calling the `signinup` API which are impossible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#err_code_001) (> `ERR_CODE_008`). ### 3. Remove the social login access token and user profile info on the backend Once you call the `signinup` API from the frontend, SuperTokens verifies the OAuth tokens and fetches the user's profile info from the third party provider. SuperTokens also links the newly created recipe user to the session user. To fetch the new user object and also the third party profile, you can override the `signinup` recipe function: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, // override the thirdparty sign in / up function signInUp: async function (input) { let existingUser: User | undefined = undefined; if (input.session !== undefined) { existingUser = await SuperTokens.getUser(input.session.getUserId()); } let response = await originalImplementation.signInUp(input); if (response.status === "OK") { let accessToken = response.oAuthTokens["access_token"]; let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; if (input.session !== undefined && response.user.id === input.session.getUserId()) { if (response.user.loginMethods.length === existingUser!.loginMethods.length + 1) { // new social account was linked to session user } else { // social account was already linked to the session // user from before } } } return response; } } } } // highlight-end }), Session.init({ /* ... */ }) ] }); ``` :::note At the moment this feature is not supported through the Go SDK. ::: :::note At the moment this feature is not supported through the Go SDK. ::: Notice in the above snippet that the check is for `input.session !== undefined && response.user.id === input.session.getUserId()`. This ensures that the custom logic runs only if it's linking a social account to your session account, and not during first factor login. --- # Post Authentication - Account Linking - Add passwords to an existing account Source: https://supertokens.com/docs/post-authentication/account-linking/add-passwords-to-an-existing-account ## Overview There may be scenarios in which you want to add a password to an account created using a social provider or passwordless login. This guide walks you through how to do this. The idea here is to reuse the existing sign up APIs, but call them with a session's access token. The APIs then create a new recipe user for that login method based on the input, and then link that to the session user. Of course, there are security checks done to ensure there is no account takeover risk, and this guide goes through them as well. ## Before you start We do not provide pre-built UI for this flow since it's probably something you want to add in your settings page or during the sign up process. This guide focuses on which APIs to call from your own UI. The frontend code snippets below refer to the `supertokens-web-js` SDK. You can continue to use this even if you have initialised the `supertokens-auth-react` SDK, on the frontend. ## Steps ### 1. Enable account linking and `emailpassword` on the backend SDK ```tsx supertokens.init({ supertokens: { connectionURI: "...", apiKey: "..." }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ // highlight-start EmailPassword.init(), AccountLinking.init({ shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { if (user === undefined) { return { shouldAutomaticallyLink: true, shouldRequireVerification: true } } if (session !== undefined && session.getUserId() === user.id) { return { shouldAutomaticallyLink: true, shouldRequireVerification: true } } return { shouldAutomaticallyLink: false } } }) // highlight-end ] }); ``` :::note At the moment this feature is not supported through the Go SDK. ::: :::note At the moment this feature is not supported through the Go SDK. ::: In the above implementation of `shouldDoAutomaticAccountLinking`, account linking is only allowed if the input session is present. This means that the system links an email password account to an existing session user. Otherwise, account linking is not allowed, which means that first factor account linking is not enabled. If you want to enable that too, you can see the [automatic account linking documentation](./automatic-account-linking). ### 2. Create a UI to show a password input to the user and handle the submit event :::important If you want to use password based auth as a second factor, or for step up auth, see the docs in the [MFA recipe](/docs/additional-verification/mfa/introduction) instead. The guide below is only meant for if you want to add a password for a user and allow them to login via email password for first factor login. ::: First, you need to detect if there already exists a password for the user. You can do this by inspecting the [user object](/docs/references/backend-sdks/user-object) on the backend and checking if there is an `emailpassword` login method. Then, if no such login method exists, you have to show a UI in which the user can add a password to their account. The [password validation documentation](/docs/authentication/email-password/customize-the-sign-up-form#change-field-validators#changing-the-default-email-and-password-validators) contains the default password validation rules. You also need to fetch the email of the user before you call the email password sign up API. You can fetch this using the user object. If the `user` object does not have an email (which can only happen if the first factor is phone OTP), then you should ask the user to go through an email OTP flow via the passwordless recipe. This step should occur before asking them to set a password. The email OTP flow also results in creating a passwordless user account and linking it to the session user. Once you have the email on the frontend, you should call the sign up API. The two big differences in the implementation are: - When you call the sign up API, you need to provide the session's access token in the request. If you are using the frontend SDK, this process happens automatically via the frontend network interceptors. The access token enables the backend to get a session and then link the email password account to session user. - New types of failure scenarios exist when calling the sign up API which are impossible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#err_code_001) (> `ERR_CODE_008`). ### 3. Check for email match in the backend sign up API Since the frontend specifies the email, verify it in the backend API before using it (since the frontend shouldn't be trusted). You can do this by overriding the email password sign up API: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ // highlight-start override: { apis: (originalImplementation) => { return { ...originalImplementation, signUpPOST: async function (input) { if (input.session !== undefined) { // this means that we are trying to add a password to the session user const inputEmail = input.formFields.find(f => f.id === "email")!.value; let sessionUserId = input.session.getUserId(); let userObject = await SuperTokens.getUser(sessionUserId); if (userObject!.emails.find(e => e === inputEmail) === undefined) { // this means that the input email does not belong to this user. return { status: "GENERAL_ERROR", message: "Cannot use this email to add a password for this user" } } } return await originalImplementation.signUpPOST!(input); } } } } // highlight-end }), Session.init({ /* ... */ }) ] }); ``` :::note At the moment this feature is not supported through the Go SDK. ::: :::note At the moment this feature is not supported through the Go SDK. ::: --- # Post Authentication - Dashboard - Setting up the dashboard Source: https://supertokens.com/docs/post-authentication/dashboard/initial-setup ## Overview The following page shows you how to set up the dashboard recipe and access the web interface. You can check the next diagram to understand how the dashboard integrates with your application. Flowchart of architecture when using SuperTokens managed service Flowchart of architecture when self-hosting SuperTokens ## Steps ### 1. Initialize the `Dashboard` recipe To get started, initialize the Dashboard recipe in the `recipeList`. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // TODO: Initialise other recipes // highlight-start Dashboard.init(), // highlight-end ], }); ``` ```go } ``` ```python from supertokens_python import init, InputAppInfo from supertokens_python.recipe import dashboard init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', # type: ignore recipe_list=[ # TODO: Initialise other recipes # highlight-start dashboard.init(), # highlight-end ] ) ``` #### Update your content security policy {{optional}} If your backend returns a `Content-Security-Policy` header, you encounter the following UI displaying the Content Security Policy violation details. Follow the instructions provided in this UI to make necessary adjustments to your backend Content Security Policy configuration. ![Content Security Policy error handled UI](/img/dashboard/csp-error.png) For example, to address the error message displayed in the above screenshot, you need to modify your `original policy`. In the given example, it appears as follows: ```text script-src: 'self' 'unsafe-inline' https://google.com img-src: https://google.com ``` To resolve this issue, make the following adjustments: ```text script-src: 'self' 'unsafe-inline' https://google.com img-src: https://google.com https://cdn.jsdelivr.net/gh/supertokens/ ``` Essentially, you need to include the domain listed as the `Blocked URI` in your violated directive block within your original policy. If you return a `Content-Security-Policy` header from your backend, you need to include the following directives for the user management dashboard to work correctly. ```text script-src: 'self' 'unsafe-inline' https://cdn.jsdelivr.net/gh/supertokens/ img-src: https://cdn.jsdelivr.net/gh/supertokens/ https://purecatamphetamine.github.io/ ``` If you return a `Content-Security-Policy` header from your backend, you need to include the following directives for the user management dashboard to work correctly. ```text script-src: 'self' 'unsafe-inline' https://cdn.jsdelivr.net/gh/supertokens/ img-src: https://cdn.jsdelivr.net/gh/supertokens/ https://purecatamphetamine.github.io/ ``` ### 2. Access the dashboard :::important The backend SDK serves the user management dashboard, and you have to use your API domain when trying to visit the dashboard. ::: Navigate to `/dashboard` to view the dashboard. :::note If you are using Next.js, upon integrating the backend SDK into your Next.js API folder, the dashboard becomes accessible by default at `/api/auth/dashboard`. For frameworks other than Next.js, access it at `/auth/dashboard`. Should you have customized the `apiBasePath` configuration property, navigate to `/dashboard` to access the dashboard. ::: Dashboard login screen UI ### 3. Create dashboard credentials :::info Paid Feature You can create 3 dashboard users* for free. If you need to create additional users: - For self hosted users, please [sign up](https://supertokens.com/auth) to generate a license key and follow the instructions sent to you by email. - For managed service users, you can click on the "enable paid features" button on [the dashboard](https://supertokens.com/dashboard-saas), and follow the steps from there on. *: A dashboard user is a user that can log into and view the user management dashboard. These users are independent to the users of your application ::: When you first set up SuperTokens, there are no credentials created for the dashboard. If you click the "Add a new user" button in the dashboard login screen you can see the command you need to execute to create credentials. Dashboard sign up screen UI To create credentials you need to make a request to SuperTokens core. - The example above uses the demo core `https://try.supertokens.com`, replace this with the connection URI you pass to the backend SDK when initialising SuperTokens. - Replace `` with your API key. If you are using a self hosted SuperTokens core there is no API key by default. In that case you can either skip or ignore the `api-key` header. - Replace `` and `` with the appropriate values. :::caution If using self-hosted SuperTokens core, you need to make sure that you add an API key to the core in case it's exposed to the internet. Otherwise, anyone can create or modify dashboard users. You can add an API key to the core by following the instructions "Auth flow customizations" > "SuperTokens core settings" > "Adding API keys" page. ::: ### 4. Update dashboard credentials You can update the email or password of existing credentials by using the "Forgot Password" button on the dashboard login page. Reset your password screen UI To update credentials you need to make a request to SuperTokens core. - The example above uses the demo core `https://try.supertokens.com`, replace this with the connection URI you pass to the backend SDK when initialising SuperTokens. - Replace `` with your API key. If you are using a self hosted SuperTokens core there is no API key by default. In that case you can either skip or ignore the `api-key` header. - Replace `` and `` with the appropriate values. You can use `newEmail` instead of `newPassword` if you want to update the email ### 5. Restrict access to dashboard users When using the dashboard recipe, you can restrict access to certain features by providing a list of emails considered as "admins." If a dashboard user logs in with an email not present in this list, they can only perform read operations. All write operations result in the backend SDKs failing the request. You can provide an array of emails to the backend SDK when initialising the dashboard recipe: :::important - Not providing an admins array results in all dashboard users having both read and write operations. - Providing an empty array as admins results in all dashboard users having only read access. ::: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // TODO: Initialise other recipes // highlight-start Dashboard.init({ admins: [ "johndoe@gmail.com", ], }), // highlight-end ], }); ``` ```go } ``` ```python from supertokens_python import init, InputAppInfo from supertokens_python.recipe import dashboard init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', # type: ignore recipe_list=[ # TODO: Initialise other recipes # highlight-start dashboard.init( admins=[ "johndoe@gmail.com", ], ), # highlight-end ] ) ``` # Post Authentication - Dashboard - User management Source: https://supertokens.com/docs/post-authentication/dashboard/user-management ## Overview With the user management dashboard you can view the list of users and their details. You can also perform different operations on these users as mentioned below. --- ## Create users If you have created your app, you may not have any users to show on the dashboard. Empty dashboard screen UI ## List users Navigate to your frontend app and create a user (via the sign-up flow). On creation, if you head back to the dashboard and refresh the page, you see that user: One user in dashboard screen UI --- ## View user details When you select a user you can view detailed information about the user such as email, phone number, user metadata, etc. User details page screen UI part one User details page screen UI part two --- ## Edit user details You can edit user information and perform actions such as resetting a user's password or revoking sessions for a user. Change password modal UI :::info Note Enable some features such as user metadata and email verification in your backend before you can use them in the user management dashboard. ::: --- ## Create user roles and permissions :::caution no-title This feature is only available through the Node.js SDK. ::: When you first use the `UserRoles` recipe, the list of roles is empty. To create roles, click on the "Add Role" button. No roles created This action opens a modal, enabling you to create a role along with its associated permissions. Permissions are essentially a list of strings assigned to a specific role. --- ## List user roles :::caution no-title This feature is only available through the Node.js SDK. ::: Create role After creating a role, the UI should display a list of all roles in your app. Roles list You can preview the role you created by clicking on the role row. The modal provides options to edit or delete the role. Preview role --- ## Assign user roles :::caution no-title This feature is only available through the Node.js SDK. ::: To assign a specific role to a user, start by finding the user in the dashboard. Upon clicking the user, navigate to the user details page where you find a section for user roles. If the selected user has associations with multiple tenants, you can choose a `tenantId` from the dropdown menu to specify the tenant for which you'd like to assign roles. Select tenant Click the edit button to start assigning roles. Then, select the "Assign Role" button, and a modal appears with a list of available roles for assignment to this user. Assign role --- ## Remove user roles :::caution no-title This feature is only available through the Node.js SDK. ::: To remove a role assigned to a user, click on the "X" icon next to that specific role. View assigned role # Post Authentication - Dashboard - Tenant management Source: https://supertokens.com/docs/post-authentication/dashboard/tenant-management ## Overview This page shows you what actions you can perform on tenants through the dashboard. :::info Caution This is only available with Node and Python SDKs. ::: Tenant Management Landing --- ## Create a new tenant Clicking on `Add Tenant` prompts you to enter the tenant id. Once you enter the tenant id, click on `Create Now` to create the tenant. You then proceed to the Tenant Details page where you can further manage the newly created tenant. Create Tenant ## View tenant details Upon selection or creation of a tenant, the Tenant Details page appears. The sections appear below. Tenant details ### Tenant ID and users The first section shows the tenant ID and the number of users in that tenant. Clicking on `See Users` takes you to the [user management page](/docs/post-authentication/dashboard/user-management) where you can view and manage the users for the selected tenant. Tenant users ### Enabled login methods This section displays the login methods available for the tenant. By enabling these toggles, you can make the corresponding login methods accessible to the users within the tenant. Appropriate recipes must be active to turn on the login methods. For example, - to turn on `emailpassword`, initialize the EmailPassword recipe in the backend. - to turn on `OTP Phone`, initialize the Passwordless recipe with `flowType` `USER_INPUT_CODE` and contactMethod `PHONE` :::info If you are using the Auth React SDK, make sure to enable [usesDynamicLoginMethods](/docs/authentication/enterprise/common-domain-login#3-tell-supertokens-about-the-saved-tenantid-from-the-previous-step) to ensure the frontend automatically shows the login methods based on the selection here. ::: Login Methods ### Secondary factors This section displays the secondary factors available for the tenant. By enabling these toggles, the corresponding factor becomes active for all users of the tenant. Refer to [MultiFactor Authentication docs](/docs/additional-verification/mfa/introduction) for more information. [MultiFactorAuth](/docs/additional-verification/mfa/initial-setup) recipe must initialize to enable Secondary Factors. Also, initialize appropriate recipes in the backend SDK to use a secondary factor. For example, - to turn on TOTP, initialize the TOTP recipe in the backend. - to turn on `OTP Phone`, initialize the Passwordless recipe with `flowType` `USER_INPUT_CODE` and contactMethod `PHONE` Secondary Factors ### Core configuration Core Configuration This section shows the current configuration values in core for the tenant. You can edit some of these settings by clicking the `pencil` icon next to the property. Edit Core Configuration :::caution Some configuration values may not be editable since they inherit from the App. If using SuperTokens managed hosting, you can modify them in the SaaS Dashboard. Else, if you are self-hosting the SuperTokens core, edit them via Docker environment variables or the `configuration.yaml` file. ::: --- ## Manage `ThirdParty` providers The Social/Enterprise providers section becomes available once `Third Party` login method is active for the tenant. Initially, configure a new provider. Add provider prompt Later on, you can configure new or existing third-party providers from the **Social/Enterprise providers** section. Social/Enterprise providers ### Configure a new provider When adding a new third-party provider, you receive a list of available options, including built-in enterprise and social providers, custom, and SAML. New Provider Upon selection of the desired provider, provide further details such as `Client ID`, `Client Secret`, etc. New Provider Details #### Enterprise providers For the Enterprise providers, provide certain extra information before proceeding to the Provider details. For example, Active Directory provider requires a `Directory ID` before editing further details. Additional configuration for Active Directory #### Custom providers If a Social/Enterprise provider is not available in the list of built-in providers, you can still use them by selecting the `Add Custom Provider` option. Start off by providing `ThirdParty ID`, `Name` and Client details such as `Client ID`, `Secret`, `Scope`, etc. Custom Provider basic details If using an OpenID compliant provider, you could add the `OIDC Discovery Endpoint`. Otherwise, configure the provider by manually providing `Authorization Endpoint`, `Token Endpoint`, `User Info Endpoint`, etc. OpenID configuration Finally, clicking on `Save` adds the Social/enterprise provider for the tenant. #### SAML providers To add a SAML provider, use the `Add SAML Provider` option. For more information on what is SAML and how it works with SuperTokens, refer [SAML docs](/docs/authentication/enterprise/saml/what-is-saml). Upon selection, provide the `Boxy URL` and the `Boxy API Key`. :::important To use SAML providers, an additional Boxy HQ service is necessary. You can either self-host yourself or email for a managed instance. Details for them are also available on this page. ::: Boxy SAML Prompt On continuing, you are further asked for the SAML configuration. You have an option to either provide SAML XML directly or via the Metadata URL from the Provider. Also, fill in other details such as `Suffix`, `Name`, `Redirect URLs` and click on `Save` to add the SAML provider. :::caution Adding ThirdParty suffix is not compulsory, however if you wish to add multiple SAML providers for a tenant, you need to add unique suffixes for each of them. ::: Boxy SAML `Config` If you did not provide the `Boxy API Key`, you need to add the `Client ID` and `Secret` obtained by calling the Boxy APIs manually. More details are [available here](/docs/authentication/enterprise/saml/boxy-hq-guide#4-upload-the-base64-xml-string-to-saml-jackson). Boxy SAML `Config` via `API` # Deployment - Migrate from MySQL to PostgreSQL Source: https://supertokens.com/docs/deployment/migrate-from-mysql ## Overview This tutorial shows you how to migrate your **SuperTokens** database from **MySQL** to **PostgreSQL**. The migration involves exporting data from MySQL, setting up a PostgreSQL database with the same schema version, and importing the data with proper format conversions. ## Before you start The tutorial assumes the following: - You have access to both your `MySQL` and `PostgreSQL` databases - Both databases are running on the same version of **SuperTokens Core** - You have administrative privileges on both databases ## Steps ### 1. Create a backup of your MySQL database Create a final backup of your MySQL database. The instructions for this step are specific to your database management system. ### 2. Prepare the PostgreSQL database Start the same version of [`supertokens-postgresql`](https://hub.docker.com/r/supertokens/supertokens-postgresql) to initialize the schema in the database. :::warning Make sure to use the same version as the `supertokens-mysql` instance that you are currently running. ::: ### 3. Export data from MySQL #### 3.1 Export standard tables Run the following command to export most of your data: ```bash mysqldump supertokens --fields-terminated-by ',' --fields-enclosed-by '"' --fields-escaped-by '\' --no-create-info --tab /var/lib/mysql-files/ ``` This creates CSV files for all tables in the `/var/lib/mysql-files/` directory. #### 3.2 Export the WebAuthn credentials table The `webauthn_credentials` table requires special handling because of the data type used to store the `public_key` field. ```sql SELECT id, app_id, rp_id, user_id, counter, HEX(public_key) AS public_key, transports, created_at, updated_at FROM webauthn_credentials INTO OUTFILE '/var/lib/mysql-files/webauthn_credentials_hex.txt' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\' LINES TERMINATED BY '\n'; ``` This exports the `public_key` field as hexadecimal text for proper conversion to PostgreSQL's Binary Data (BYTEA) format. ### 4. Transfer data files If necessary, copy the exported CSV files to a location where the `PostgreSQL` database can access them. ### 5. Import data into the PostgreSQL database Next, you need to import the data into your PostgreSQL database. #### 5.1. Disable triggers Connect to your PostgreSQL database and disable triggers to prevent constraint violations during import. ```sql SET session_replication_role = 'replica'; ``` #### 5.2 Import the standard tables For most tables, you can import the data directly. ```sql COPY app_id_to_user_id FROM '/pg-data-host/app_id_to_user_id.txt' CSV DELIMITER ',' QUOTE '"' ESCAPE '\' NULL as '\N'; ``` ```sql COPY app_id_to_user_id(app_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id) FROM '/pg-data-host/app_id_to_user_id.txt' CSV DELIMITER ',' QUOTE '"' ESCAPE '\' NULL as '\N'; ``` #### 5.3 Handle the third-party provider clients table The `tenant_thirdparty_provider_clients` table requires special handling. You need to do this to differences between the `MySQL` `JSON` and `PostgreSQL` `text[]` formats. ## Create a staging table ```sql CREATE TABLE tenant_thirdparty_provider_clients_raw ( connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, third_party_id VARCHAR(28) NOT NULL, client_type VARCHAR(64) DEFAULT '' NOT NULL, client_id VARCHAR(256) NOT NULL, client_secret TEXT, scope JSONB, force_pkce BOOLEAN, additional_config TEXT ); ``` ## Import data into the staging table: ```sql COPY tenant_thirdparty_provider_clients_raw FROM '/host/tenant_thirdparty_provider_clients.txt' CSV DELIMITER ',' QUOTE '"' ESCAPE '\' NULL as '\N'; ``` ## Convert and insert into the final table: ```sql INSERT INTO tenant_thirdparty_provider_clients ( connection_uri_domain, app_id, tenant_id, third_party_id, client_type, client_id, client_secret, force_pkce, additional_config, scope ) SELECT connection_uri_domain, app_id, tenant_id, third_party_id, client_type, client_id, client_secret, force_pkce, additional_config, ARRAY( SELECT jsonb_array_elements_text(scope->jsonb_object_keys(scope)) ) FROM tenant_thirdparty_provider_clients_raw; ``` #### 5.4 Handle the WebAuthn credentials table The `webauthn_credentials` table requires conversion from MySQL Binary Large Object, `BLOB`, to PostgreSQL Binary Data, `BYTEA` format. ## Create a staging table ```sql CREATE TABLE IF NOT EXISTS webauthn_credentials_staging ( id VARCHAR(256) NOT NULL, app_id VARCHAR(64) DEFAULT 'public' NOT NULL, rp_id VARCHAR(256) NOT NULL, user_id CHAR(36), counter BIGINT NOT NULL, public_key TEXT NOT NULL, transports TEXT NOT NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL ); ``` ## Import the hexadecimal data ```sql COPY webauthn_credentials_staging FROM '/host/webauthn_credentials_hex.txt' CSV DELIMITER ',' QUOTE '"' ESCAPE '\' NULL as '\N'; ``` ## Convert and insert into the final table ```sql INSERT INTO webauthn_credentials ( id, app_id, rp_id, user_id, counter, public_key, transports, created_at, updated_at ) SELECT id, app_id, rp_id, user_id, counter, decode(public_key, 'hex'), transports, created_at, updated_at FROM webauthn_credentials_staging; ``` #### 5.5 Delete the staging tables Delete the two temporary tables. ``` DROP TABLE webauthn_credentials_staging; DROP TABLE tenant_thirdparty_provider_clients_raw; ``` #### 5.6 Re-enable triggers After importing all data, re-enable the triggers: ```sql SET session_replication_role = 'origin'; ``` ### 6. Verify the migration Verify that all data migrated successfully by comparing record counts between your MySQL and PostgreSQL databases: ```sql -- Run on both databases SELECT COUNT(*) FROM users; SELECT COUNT(*) FROM sessions; -- Add other tables as needed ``` If the numbers match, you have successfully migrated your SuperTokens data from `MySQL` to `PostgreSQL` :tada: # Deployment - Self-hosting SuperTokens Source: https://supertokens.com/docs/deployment/self-host-supertokens See how you can run **SuperTokens** in your own infrastructure. ## Overview One of the main features of **SuperTokens** is that you can run it using your own resources. This way you have full control over the authentication data and you can scale based on your needs. ## Before you start To deploy the Core Service you must configure two things: the actual API and the database. - The core service can be deployed using a **Docker** image or directly inside your VM. - The supported database is **PostgreSQL**. The minimum required version is `13.0`. :::info **SuperTokens Core** has dropped **MySQL** and **MongoDB** support with the `11.0.0` release. If you want to reference the old documentation, please [open this page](/docs/legacy/core/v10/self-host-supertokens). ::: ## Steps ### 1. Install SuperTokens core #### With docker ```bash docker run -p 3567:3567 -d registry.supertokens.io/supertokens/supertokens-postgresql:latest ``` - To see all the environment variables available, please see [the README file](https://github.com/supertokens/supertokens-docker-postgresql/blob/master/README.md). - The above command starts the container with an in-memory database. This means you **do not need to connect it to PostgreSQL to test out SuperTokens**. #### Without docker ##### 1. Download SuperTokens ## Visit the [open source download page](https://SuperTokens.com/use-oss). ## Click on the "Binary" tab. ## Choose your database. ## Download the SuperTokens zip file for your OS. Once downloaded, remove the zip, and you see a folder named `supertokens`. ##### 2. Install SuperTokens ```bash # sudo is required so that the supertokens # command can be added to your PATH variable. cd supertokens sudo ./install ``` ```bash cd supertokens ./install ``` :::caution You may get an error like `java cannot be opened because the developer cannot be verified`. To solve this, visit System Preferences > Security & Privacy > General Tab, and then click on the Allow button at the bottom. Then retry the command above. ::: ```batch Rem run as an Administrator. This is required so that the supertokens Rem command can be added to your PATH. cd supertokens install.bat ``` :::important After installing, you can delete the downloaded folder as you no longer need it. Make any changes to the configuration in the `config.yaml` file in the installation directory, as specified in the output of the `supertokens --help` command. ::: ##### 3. Start the core service Running the following command starts the service. ```bash supertokens start [--host=...] [--port=...] ``` - The above command starts the container with an in-memory database. - To see all available options please run `supertokens start --help` :::info Tip To stop the service, run the following command: ```bash supertokens stop ``` ::: ### 2. Test that the service is running Open a browser and visit `http://localhost:3567/hello`. If you see a page that says `Hello` back, then the container started successfully! If you are having issues with starting the docker image, please feel free to reach out [over email](mailto:support@supertokens.com) or [via Discord](https://supertokens.com/discord). :::tip The `/hello` route checks whether the database connection is correctly set up and only returns a 200 status code if there is no issue. If you are using Kubernetes or docker swarm, this endpoint is perfect for doing readiness and liveness probes. ::: ### 3. Connect the backend SDK with SuperTokens - The default `port` for SuperTokens is `3567`. You can change this by binding a different port in the `docker run` command. For example, `docker run -p 8080:3567` runs SuperTokens on port `8080` on your machine. - The connection info goes in the `supertokens` object in the `init` function on your backend: ```tsx supertokens.init({ // highlight-start supertokens: { connectionURI: "http://localhost:3567", apiKey: "someKey" // OR can be undefined }, // highlight-end appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [] }); ``` ```go ``` You can skip this step if you want SuperTokens to write to your own database. In this case, you need to provide your database's name as shown in the step below. #### 4.2 Connect SuperTokens to your database ##### With docker :::caution Host being `localhost` / `127.0.0.1` does not work in a docker image. Instead, please provide the database's local / public host name or IP address. You also need to make the database listen on all the IPs of the local machine. Edit the `postgresql.conf` configuration file and set the value of `listen_addresses` to `0.0.0.0`. ::: :::caution It is important to use the `postgresql://` scheme designator in the PostgreSQL Connection URI. Using `postgres://` will lead to a startup error. ::: ```bash docker run \ -p 3567:3567 \ // highlight-next-line -e POSTGRESQL_CONNECTION_URI="postgresql://username:pass@host/dbName" \ -d registry.supertokens.io/supertokens/supertokens-postgresql # OR docker run \ -p 3567:3567 \ // highlight-start -e POSTGRESQL_USER="username" \ -e POSTGRESQL_PASSWORD="password" \ -e POSTGRESQL_HOST="host" \ -e POSTGRESQL_PORT="5432" \ -e POSTGRESQL_DATABASE_NAME="supertokens" \ // highlight-end -d registry.supertokens.io/supertokens/supertokens-postgresql ``` :::tip You can also provide the table schema by setting the `POSTGRESQL_TABLE_SCHEMA` option. ::: ##### Without docker ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command postgresql_connection_uri: "postgresql://username:pass@host/dbName" # OR postgresql_user: "username" postgresql_password: "password" postgresql_host: "host" postgresql_port: "5432" postgresql_database_name: "supertokens" ``` You can also provide the table schema by setting the `postgresql_table_schema` option. :::info The required tables should create automatically if the database user has table creation permission. If not, you can create them manually using the following snippet. ## Database tables ```sql CREATE TABLE apps ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, created_at_time BIGINT, CONSTRAINT apps_pkey PRIMARY KEY (app_id) ); CREATE TABLE tenants ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, created_at_time BIGINT, CONSTRAINT tenants_pkey PRIMARY KEY (app_id, tenant_id), CONSTRAINT tenants_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE ); CREATE INDEX tenants_app_id_index ON tenants (app_id); CREATE TABLE tenant_configs ( connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, core_config TEXT, email_password_enabled BOOLEAN, passwordless_enabled BOOLEAN, third_party_enabled BOOLEAN, is_first_factors_null BOOLEAN, CONSTRAINT tenant_configs_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id) ); CREATE TABLE tenant_thirdparty_providers ( connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, third_party_id VARCHAR(28) NOT NULL, name VARCHAR(64), authorization_endpoint TEXT, authorization_endpoint_query_params TEXT, token_endpoint TEXT, token_endpoint_body_params TEXT, user_info_endpoint TEXT, user_info_endpoint_query_params TEXT, user_info_endpoint_headers TEXT, jwks_uri TEXT, oidc_discovery_endpoint TEXT, require_email BOOLEAN, user_info_map_from_id_token_payload_user_id VARCHAR(64), user_info_map_from_id_token_payload_email VARCHAR(64), user_info_map_from_id_token_payload_email_verified VARCHAR(64), user_info_map_from_user_info_endpoint_user_id VARCHAR(64), user_info_map_from_user_info_endpoint_email VARCHAR(64), user_info_map_from_user_info_endpoint_email_verified VARCHAR(64), CONSTRAINT tenant_thirdparty_providers_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id, third_party_id), CONSTRAINT tenant_thirdparty_providers_tenant_id_fkey FOREIGN KEY (connection_uri_domain, app_id, tenant_id) REFERENCES public.tenant_configs(connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE ); CREATE INDEX tenant_thirdparty_providers_tenant_id_index ON tenant_thirdparty_providers (connection_uri_domain, app_id, tenant_id); CREATE TABLE tenant_thirdparty_provider_clients ( connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, third_party_id VARCHAR(28) NOT NULL, client_type VARCHAR(64) DEFAULT '' NOT NULL, client_id VARCHAR(256) NOT NULL, client_secret TEXT, scope VARCHAR(128)[], force_pkce BOOLEAN, additional_config TEXT, CONSTRAINT tenant_thirdparty_provider_clients_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id, third_party_id, client_type), CONSTRAINT tenant_thirdparty_provider_clients_third_party_id_fkey FOREIGN KEY (connection_uri_domain, app_id, tenant_id, third_party_id) REFERENCES public.tenant_thirdparty_providers(connection_uri_domain, app_id, tenant_id, third_party_id) ON DELETE CASCADE ); CREATE INDEX tenant_thirdparty_provider_clients_third_party_id_index ON tenant_thirdparty_provider_clients (connection_uri_domain, app_id, tenant_id, third_party_id); CREATE TABLE tenant_first_factors ( connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, factor_id VARCHAR(128), CONSTRAINT tenant_first_factors_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id, factor_id), CONSTRAINT tenant_first_factors_tenant_id_fkey FOREIGN KEY (connection_uri_domain, app_id, tenant_id) REFERENCES public.tenant_configs(connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS tenant_first_factors_tenant_id_index ON tenant_first_factors (connection_uri_domain, app_id, tenant_id); CREATE TABLE tenant_required_secondary_factors ( connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, factor_id VARCHAR(128), CONSTRAINT tenant_required_secondary_factors_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id, factor_id), CONSTRAINT tenant_required_secondary_factors_tenant_id_fkey FOREIGN KEY (connection_uri_domain, app_id, tenant_id) REFERENCES public.tenant_configs(connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS tenant_default_required_factor_ids_tenant_id_index ON tenant_required_secondary_factors (connection_uri_domain, app_id, tenant_id); CREATE TABLE key_value ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, name VARCHAR(128) NOT NULL, value TEXT, created_at_time BIGINT, CONSTRAINT key_value_pkey PRIMARY KEY (app_id, tenant_id, name), CONSTRAINT key_value_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE ); CREATE INDEX key_value_tenant_id_index ON key_value (app_id, tenant_id); CREATE TABLE app_id_to_user_id ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id character(36) NOT NULL, recipe_id VARCHAR(128) NOT NULL, primary_or_recipe_user_id CHAR(36) NOT NULL, is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT app_id_to_user_id_pkey PRIMARY KEY (app_id, user_id), CONSTRAINT app_id_to_user_id_primary_or_recipe_user_id_fkey FOREIGN KEY(app_id, primary_or_recipe_user_id) REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE, CONSTRAINT app_id_to_user_id_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE ); CREATE INDEX app_id_to_user_id_app_id_index ON app_id_to_user_id (app_id); CREATE INDEX app_id_to_user_id_primary_user_id_index ON app_id_to_user_id (primary_or_recipe_user_id, app_id); CREATE TABLE all_auth_recipe_users ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id character(36) NOT NULL, primary_or_recipe_user_id CHAR(36) NOT NULL, is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE, recipe_id VARCHAR(128) NOT NULL, time_joined BIGINT NOT NULL, primary_or_recipe_user_time_joined BIGINT NOT NULL, CONSTRAINT all_auth_recipe_users_pkey PRIMARY KEY (app_id, tenant_id, user_id), CONSTRAINT all_auth_recipe_users_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE, CONSTRAINT all_auth_recipe_users_primary_or_recipe_user_id_fkey FOREIGN KEY(app_id, primary_or_recipe_user_id) REFERENCES public.app_id_to_user_id (app_id, user_id) ON DELETE CASCADE, CONSTRAINT all_auth_recipe_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE ); CREATE INDEX all_auth_recipe_users_pagination_index1 ON all_auth_recipe_users (app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC); CREATE INDEX all_auth_recipe_users_pagination_index2 ON all_auth_recipe_users (app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC); CREATE INDEX all_auth_recipe_users_pagination_index3 ON all_auth_recipe_users (recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC); CREATE INDEX all_auth_recipe_users_pagination_index4 ON all_auth_recipe_users (recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC); CREATE INDEX all_auth_recipe_users_primary_user_id_index ON all_auth_recipe_users (primary_or_recipe_user_id, app_id); CREATE INDEX all_auth_recipe_users_recipe_id_index ON all_auth_recipe_users (app_id, recipe_id, tenant_id); CREATE INDEX all_auth_recipe_user_id_index ON all_auth_recipe_users (app_id, user_id); CREATE INDEX all_auth_recipe_tenant_id_index ON all_auth_recipe_users (app_id, tenant_id); CREATE TABLE userid_mapping ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, supertokens_user_id character(36) NOT NULL, external_user_id VARCHAR(128) NOT NULL, external_user_id_info TEXT, CONSTRAINT userid_mapping_external_user_id_key UNIQUE (app_id, external_user_id), CONSTRAINT userid_mapping_pkey PRIMARY KEY (app_id, supertokens_user_id, external_user_id), CONSTRAINT userid_mapping_supertokens_user_id_key UNIQUE (app_id, supertokens_user_id), CONSTRAINT userid_mapping_supertokens_user_id_fkey FOREIGN KEY (app_id, supertokens_user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE ); CREATE INDEX userid_mapping_supertokens_user_id_index ON userid_mapping (app_id, supertokens_user_id); CREATE TABLE dashboard_users ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id character(36) NOT NULL, email VARCHAR(256) NOT NULL, password_hash VARCHAR(256) NOT NULL, time_joined BIGINT NOT NULL, CONSTRAINT dashboard_users_email_key UNIQUE (app_id, email), CONSTRAINT dashboard_users_pkey PRIMARY KEY (app_id, user_id), CONSTRAINT dashboard_users_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE ); CREATE INDEX dashboard_users_app_id_index ON dashboard_users (app_id); CREATE TABLE dashboard_user_sessions ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, session_id character(36) NOT NULL, user_id character(36) NOT NULL, time_created BIGINT NOT NULL, expiry BIGINT NOT NULL, CONSTRAINT dashboard_user_sessions_pkey PRIMARY KEY (app_id, session_id), CONSTRAINT dashboard_user_sessions_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.dashboard_users(app_id, user_id) ON UPDATE CASCADE ON DELETE CASCADE ); CREATE INDEX dashboard_user_sessions_expiry_index ON dashboard_user_sessions (expiry); CREATE INDEX dashboard_user_sessions_user_id_index ON dashboard_user_sessions (app_id, user_id); CREATE TABLE session_access_token_signing_keys ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, created_at_time BIGINT NOT NULL, value TEXT, CONSTRAINT session_access_token_signing_keys_pkey PRIMARY KEY (app_id, created_at_time), CONSTRAINT session_access_token_signing_keys_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE ); CREATE INDEX access_token_signing_keys_app_id_index ON session_access_token_signing_keys (app_id); CREATE TABLE session_info ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, session_handle VARCHAR(255) NOT NULL, user_id VARCHAR(128) NOT NULL, refresh_token_hash_2 VARCHAR(128) NOT NULL, session_data TEXT, expires_at BIGINT NOT NULL, created_at_time BIGINT NOT NULL, jwt_user_payload TEXT, use_static_key BOOLEAN NOT NULL, CONSTRAINT session_info_pkey PRIMARY KEY (app_id, tenant_id, session_handle), CONSTRAINT session_info_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE ); CREATE INDEX session_expiry_index ON session_info (expires_at); CREATE INDEX session_info_tenant_id_index ON session_info (app_id, tenant_id); CREATE TABLE user_last_active ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id VARCHAR(128) NOT NULL, last_active_time BIGINT, CONSTRAINT user_last_active_pkey PRIMARY KEY (app_id, user_id), CONSTRAINT user_last_active_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE ); CREATE INDEX user_last_active_app_id_index ON user_last_active (app_id); CREATE TABLE emailpassword_users ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id character(36) NOT NULL, email VARCHAR(256) NOT NULL, password_hash VARCHAR(256) NOT NULL, time_joined BIGINT NOT NULL, CONSTRAINT emailpassword_users_pkey PRIMARY KEY (app_id, user_id), CONSTRAINT emailpassword_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE ); CREATE TABLE emailpassword_user_to_tenant ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id character(36) NOT NULL, email VARCHAR(256) NOT NULL, CONSTRAINT emailpassword_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email), CONSTRAINT emailpassword_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id), CONSTRAINT emailpassword_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES public.all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE ); CREATE TABLE emailpassword_pswd_reset_tokens ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id character(36) NOT NULL, token VARCHAR(128) NOT NULL, token_expiry BIGINT NOT NULL, email VARCHAR(256), CONSTRAINT emailpassword_pswd_reset_tokens_pkey PRIMARY KEY (app_id, user_id, token), CONSTRAINT emailpassword_pswd_reset_tokens_token_key UNIQUE (token), CONSTRAINT emailpassword_pswd_reset_tokens_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON UPDATE CASCADE ON DELETE CASCADE ); CREATE INDEX emailpassword_password_reset_token_expiry_index ON emailpassword_pswd_reset_tokens (token_expiry); CREATE INDEX emailpassword_pswd_reset_tokens_user_id_index ON emailpassword_pswd_reset_tokens (app_id, user_id); CREATE TABLE emailverification_verified_emails ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id VARCHAR(128) NOT NULL, email VARCHAR(256) NOT NULL, CONSTRAINT emailverification_verified_emails_pkey PRIMARY KEY (app_id, user_id, email), CONSTRAINT emailverification_verified_emails_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE ); CREATE INDEX emailverification_verified_emails_app_id_index ON emailverification_verified_emails (app_id); CREATE TABLE emailverification_tokens ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id VARCHAR(128) NOT NULL, email VARCHAR(256) NOT NULL, token VARCHAR(128) NOT NULL, token_expiry BIGINT NOT NULL, CONSTRAINT emailverification_tokens_pkey PRIMARY KEY (app_id, tenant_id, user_id, email, token), CONSTRAINT emailverification_tokens_token_key UNIQUE (token), CONSTRAINT emailverification_tokens_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE ); CREATE INDEX emailverification_tokens_index ON emailverification_tokens (token_expiry); CREATE INDEX emailverification_tokens_tenant_id_index ON emailverification_tokens (app_id, tenant_id); CREATE TABLE thirdparty_users ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, third_party_id VARCHAR(28) NOT NULL, third_party_user_id VARCHAR(256) NOT NULL, user_id character(36) NOT NULL, email VARCHAR(256) NOT NULL, time_joined BIGINT NOT NULL, CONSTRAINT thirdparty_users_pkey PRIMARY KEY (app_id, user_id), CONSTRAINT thirdparty_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE ); CREATE INDEX thirdparty_users_email_index ON thirdparty_users (app_id, email); CREATE INDEX thirdparty_users_thirdparty_user_id_index ON thirdparty_users (app_id, third_party_id, third_party_user_id); CREATE TABLE thirdparty_user_to_tenant ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id character(36) NOT NULL, third_party_id VARCHAR(28) NOT NULL, third_party_user_id VARCHAR(256) NOT NULL, CONSTRAINT thirdparty_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id), CONSTRAINT thirdparty_user_to_tenant_third_party_user_id_key UNIQUE (app_id, tenant_id, third_party_id, third_party_user_id), CONSTRAINT thirdparty_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES public.all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE ); CREATE TABLE passwordless_users ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id character(36) NOT NULL, email VARCHAR(256), phone_number VARCHAR(256), time_joined BIGINT NOT NULL, CONSTRAINT passwordless_users_pkey PRIMARY KEY (app_id, user_id), CONSTRAINT passwordless_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE ); CREATE TABLE passwordless_user_to_tenant ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id character(36) NOT NULL, email VARCHAR(256), phone_number VARCHAR(256), CONSTRAINT passwordless_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email), CONSTRAINT passwordless_user_to_tenant_phone_number_key UNIQUE (app_id, tenant_id, phone_number), CONSTRAINT passwordless_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id), CONSTRAINT passwordless_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES public.all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE ); CREATE TABLE passwordless_devices ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, device_id_hash character(44) NOT NULL, email VARCHAR(256), phone_number VARCHAR(256), link_code_salt character(44) NOT NULL, failed_attempts integer NOT NULL, CONSTRAINT passwordless_devices_pkey PRIMARY KEY (app_id, tenant_id, device_id_hash), CONSTRAINT passwordless_devices_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE ); CREATE INDEX passwordless_devices_email_index ON passwordless_devices (app_id, tenant_id, email); CREATE INDEX passwordless_devices_phone_number_index ON passwordless_devices (app_id, tenant_id, phone_number); CREATE INDEX passwordless_devices_tenant_id_index ON passwordless_devices (app_id, tenant_id); CREATE TABLE passwordless_codes ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, code_id character(36) NOT NULL, device_id_hash character(44) NOT NULL, link_code_hash character(44) NOT NULL, created_at BIGINT NOT NULL, CONSTRAINT passwordless_codes_link_code_hash_key UNIQUE (app_id, tenant_id, link_code_hash), CONSTRAINT passwordless_codes_pkey PRIMARY KEY (app_id, tenant_id, code_id), CONSTRAINT passwordless_codes_device_id_hash_fkey FOREIGN KEY (app_id, tenant_id, device_id_hash) REFERENCES public.passwordless_devices(app_id, tenant_id, device_id_hash) ON UPDATE CASCADE ON DELETE CASCADE ); CREATE INDEX passwordless_codes_created_at_index ON passwordless_codes (app_id, tenant_id, created_at); CREATE INDEX passwordless_codes_device_id_hash_index ON passwordless_codes (app_id, tenant_id, device_id_hash); CREATE TABLE jwt_signing_keys ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, key_id VARCHAR(255) NOT NULL, key_string TEXT NOT NULL, algorithm VARCHAR(10) NOT NULL, created_at BIGINT, CONSTRAINT jwt_signing_keys_pkey PRIMARY KEY (app_id, key_id), CONSTRAINT jwt_signing_keys_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE ); CREATE INDEX jwt_signing_keys_app_id_index ON jwt_signing_keys (app_id); CREATE TABLE user_metadata ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id VARCHAR(128) NOT NULL, user_metadata TEXT NOT NULL, CONSTRAINT user_metadata_pkey PRIMARY KEY (app_id, user_id), CONSTRAINT user_metadata_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE ); CREATE INDEX user_metadata_app_id_index ON user_metadata (app_id); CREATE TABLE roles ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, role VARCHAR(255) NOT NULL, CONSTRAINT roles_pkey PRIMARY KEY (app_id, role), CONSTRAINT roles_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE ); CREATE INDEX roles_app_id_index ON roles (app_id); CREATE TABLE role_permissions ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, role VARCHAR(255) NOT NULL, permission VARCHAR(255) NOT NULL, CONSTRAINT role_permissions_pkey PRIMARY KEY (app_id, role, permission), CONSTRAINT role_permissions_role_fkey FOREIGN KEY (app_id, role) REFERENCES public.roles(app_id, role) ON DELETE CASCADE ); CREATE INDEX role_permissions_permission_index ON role_permissions (app_id, permission); CREATE INDEX role_permissions_role_index ON role_permissions (app_id, role); CREATE TABLE user_roles ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id VARCHAR(128) NOT NULL, role VARCHAR(255) NOT NULL, CONSTRAINT user_roles_pkey PRIMARY KEY (app_id, tenant_id, user_id, role), CONSTRAINT user_roles_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE ); CREATE INDEX user_roles_role_index ON user_roles (app_id, tenant_id, role); CREATE INDEX user_roles_tenant_id_index ON user_roles (app_id, tenant_id); CREATE INDEX user_roles_app_id_role_index ON user_roles (app_id, role); CREATE TABLE totp_users ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id VARCHAR(128) NOT NULL, CONSTRAINT totp_users_pkey PRIMARY KEY (app_id, user_id), CONSTRAINT totp_users_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE ); CREATE INDEX totp_users_app_id_index ON totp_users (app_id); CREATE TABLE totp_user_devices ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id VARCHAR(128) NOT NULL, device_name VARCHAR(256) NOT NULL, secret_key VARCHAR(256) NOT NULL, period integer NOT NULL, skew integer NOT NULL, verified BOOLEAN NOT NULL, created_at BIGINT, CONSTRAINT totp_user_devices_pkey PRIMARY KEY (app_id, user_id, device_name), CONSTRAINT totp_user_devices_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.totp_users(app_id, user_id) ON DELETE CASCADE ); CREATE INDEX totp_user_devices_user_id_index ON totp_user_devices (app_id, user_id); CREATE TABLE totp_used_codes ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id VARCHAR(128) NOT NULL, code VARCHAR(8) NOT NULL, is_valid BOOLEAN NOT NULL, expiry_time_ms BIGINT NOT NULL, created_time_ms BIGINT NOT NULL, CONSTRAINT totp_used_codes_pkey PRIMARY KEY (app_id, tenant_id, user_id, created_time_ms), CONSTRAINT totp_used_codes_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE, CONSTRAINT totp_used_codes_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.totp_users(app_id, user_id) ON DELETE CASCADE ); CREATE INDEX totp_used_codes_expiry_time_ms_index ON totp_used_codes (app_id, tenant_id, expiry_time_ms); CREATE INDEX totp_used_codes_tenant_id_index ON totp_used_codes (app_id, tenant_id); CREATE INDEX totp_used_codes_user_id_index ON totp_used_codes (app_id, user_id); CREATE TABLE IF NOT EXISTS bulk_import_users ( id CHAR(36), app_id VARCHAR(64) NOT NULL DEFAULT 'public', primary_user_id VARCHAR(36), raw_data TEXT NOT NULL, status VARCHAR(128) DEFAULT 'NEW', error_msg TEXT, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, CONSTRAINT bulk_import_users_pkey PRIMARY KEY(app_id, id), CONSTRAINT bulk_import_users__app_id_fkey FOREIGN KEY(app_id) REFERENCES apps(app_id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS bulk_import_users_status_updated_at_index ON bulk_import_users (app_id, status, updated_at); CREATE INDEX IF NOT EXISTS bulk_import_users_pagination_index1 ON bulk_import_users (app_id, status, created_at DESC, id DESC); CREATE INDEX IF NOT EXISTS bulk_import_users_pagination_index2 ON bulk_import_users (app_id, created_at DESC, id DESC); CREATE INDEX IF NOT EXISTS session_info_user_id_app_id_index ON session_info (user_id, app_id); CREATE INDEX IF NOT EXISTS emailverification_verified_emails_app_id_email_index ON emailverification_verified_emails (app_id, email); CREATE TABLE IF NOT EXISTS webauthn_account_recovery_tokens ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id CHAR(36) NOT NULL, email VARCHAR(256) NOT NULL, token VARCHAR(256) NOT NULL, expires_at BIGINT NOT NULL, CONSTRAINT webauthn_account_recovery_token_pkey PRIMARY KEY (app_id, tenant_id, user_id, token), CONSTRAINT webauthn_account_recovery_token_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS webauthn_credentials ( id VARCHAR(256) NOT NULL, app_id VARCHAR(64) DEFAULT 'public' NOT NULL, rp_id VARCHAR(256) NOT NULL, user_id CHAR(36), counter BIGINT NOT NULL, public_key BYTEA NOT NULL, transports TEXT NOT NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, CONSTRAINT webauthn_credentials_pkey PRIMARY KEY (app_id, rp_id, id), CONSTRAINT webauthn_credentials_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES webauthn_users (app_id, user_id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS webauthn_generated_options ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public'NOT NULL, id CHAR(36) NOT NULL, challenge VARCHAR(256) NOT NULL, email VARCHAR(256), rp_id VARCHAR(256) NOT NULL, rp_name VARCHAR(256) NOT NULL, origin VARCHAR(256) NOT NULL, expires_at BIGINT NOT NULL, created_at BIGINT NOT NULL, user_presence_required BOOLEAN DEFAULT false NOT NULL, user_verification VARCHAR(12) DEFAULT 'preferred' NOT NULL, CONSTRAINT webauthn_generated_options_pkey PRIMARY KEY (app_id, tenant_id, id), CONSTRAINT webauthn_generated_options_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS webauthn_user_to_tenant ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id CHAR(36) NOT NULL, email VARCHAR(256) NOT NULL, CONSTRAINT webauthn_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email), CONSTRAINT webauthn_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id), CONSTRAINT webauthn_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS webauthn_users ( app_id VARCHAR(64) DEFAULT 'public' NOT NULL, user_id CHAR(36) NOT NULL, email VARCHAR(256) NOT NULL, rp_id VARCHAR(256) NOT NULL, time_joined BIGINT NOT NULL, CONSTRAINT webauthn_users_pkey PRIMARY KEY (app_id, user_id), CONSTRAINT webauthn_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES app_id_to_user_id(app_id, user_id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS webauthn_user_to_tenant_email_index ON webauthn_user_to_tenant (app_id, email); CREATE INDEX IF NOT EXISTS webauthn_user_challenges_expires_at_index ON webauthn_generated_options (app_id, tenant_id, expires_at); CREATE INDEX IF NOT EXISTS webauthn_credentials_user_id_index ON webauthn_credentials (user_id); CREATE INDEX IF NOT EXISTS webauthn_account_recovery_token_token_index ON webauthn_account_recovery_tokens (app_id, tenant_id, token); CREATE INDEX IF NOT EXISTS webauthn_account_recovery_token_expires_at_index ON webauthn_account_recovery_tokens (expires_at DESC); CREATE INDEX IF NOT EXISTS webauthn_account_recovery_token_email_index ON webauthn_account_recovery_tokens (app_id, tenant_id, email); ``` ::: #### 4.3 Test the connection To test, start SuperTokens and run the following query in your database ```sql SELECT * FROM key_value; ``` If you see at least one row, it means that the connection has been successfully completed! #### 4.4 Rename database tables {{optional}} :::caution If you already have tables created by SuperTokens, and then you rename them, SuperTokens creates new tables. Please be sure to migrate the data from the existing one to the new one. ::: You can add a prefix to all table names that SuperTokens manages. This way, all will be renamed in a way that has no clashes with your tables. For example, two tables created by SuperTokens have the names `emailpassword_users` and `thirdparty_users`. If you add a prefix to them (something like `"my_prefix"`), then the tables become `my_prefix_emailpassword_users` and `my_prefix_thirdparty_users`. ```bash docker run \ -p 3567:3567 \ // highlight-next-line -e POSTGRESQL_TABLE_NAMES_PREFIX="my_prefix" \ -d registry.supertokens.io/supertokens/supertokens-postgresql ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command postgresql_table_names_prefix: "my_prefix" ``` ### 5. Add license keys To access some features in your self-hosted service you must use **license keys**. You can sign up on [**SuperTokens**](https://supertokens.com/auth) to receive one. Once you have the license key you need to manually add it to your **SuperTokens Core Instance**. To do this you have to call the Core API with the following request: ```bash title="Add License Key" showAppTypeSelect curl --location --request PUT /ee/license \ --header 'Content-Type: application/json' \ --header 'api-key: ^{coreInfo.key}' \ --data-raw '{ "licenseKey": "" }' ``` ## References ### Docker compose file ```bash version: '3' services: # Note: If you are assigning a custom name to your db service on the line below, make sure it does not contain underscores db: image: 'postgres:latest' environment: POSTGRES_USER: supertokens_user POSTGRES_PASSWORD: somePassword POSTGRES_DB: supertokens ports: - 5432:5432 networks: - app_network restart: unless-stopped healthcheck: test: ['CMD', 'pg_isready', '-U', 'supertokens_user', '-d', 'supertokens'] interval: 5s timeout: 5s retries: 5 supertokens: image: registry.supertokens.io/supertokens/supertokens-postgresql:latest depends_on: db: condition: service_healthy ports: - 3567:3567 environment: POSTGRESQL_CONNECTION_URI: "postgresql://supertokens_user:somePassword@db:5432/supertokens" networks: - app_network restart: unless-stopped healthcheck: test: > bash -c 'exec 3<>/dev/tcp/127.0.0.1/3567 && echo -e "GET /hello HTTP/1.1\r\nhost: 127.0.0.1:3567\r\nConnection: close\r\n\r\n" >&3 && cat <&3 | grep "Hello"' interval: 10s timeout: 5s retries: 5 networks: app_network: driver: bridge ``` :::important If you are running the backend process that integrates with the backend SDK as part of the docker compose file as well, make sure to use `http://supertokens:3567` as the connection URI instead of `http://localhost:3567`. ::: ### Helm charts for Kubernetes - For [PostgreSQL image](https://github.com/supertokens/supertokens-docker-postgresql/tree/master/helm-chart) # Deployment - Rate limit policy Source: https://supertokens.com/docs/deployment/rate-limits ## Overview The following page describes how rate limits apply during SuperTokens API calls. ## For managed service The SuperTokens core of a managed account is rate limited on a per app and per IP address basis. This means that if you query the core for `app1` using the same IP address, the rate limit kicks in, and you get a `429` status code back from the core. However, if you query the core using different IP addresses or for a different app, the rate limit of that does not interfere with the previous requests (that had another IP or was for another app). ### Free tier The free tier of the managed service has a rate limit of 50 requests per second with a burst of 50 requests per second (with no delay). This should be enough for 5-10 concurrent sign in / up (each sign in API call queries the SuperTokens core multiple times). :::important The backend SDK auto retries if it gets a `429` status code from the core (up to 5 times before throwing an error). ::: ### Paying users If you are a paying user for SuperTokens, the rate limit and the burst limit adjust dynamically based on your usage (with a minimum of a 100rps). You should not see any `429`s unless there is a **significant** spike in requests. :::info Paid Feature If you want higher rate limits, please [email support](mailto:support@supertokens.com), requesting a higher rate limit. ::: ### Special case The `/hello` API exposed by the core is commonly used for health checks. This API does not require any API key, and has its own rate limit of 5 requests per second per app (regardless of the IP address querying it). This is independent to the rate limit described above, and cannot change. ## For self hosted The SuperTokens core has no rate limit other than for the `/hello` API (which is 5 requests per second per app). You are free to add rate limits to the core by using [a reverse proxy like Nginx](https://www.nginx.com/blog/rate-limiting-nginx/). If you want to implement rate limiting policy similar to the managed service described above, add the following to your `http` and `server` block in the `nginx.conf` file: ```text http { # other configs.. map $request_uri $limit_req_zone_key { "~^/(appId|appid)-(\w+)/?" $binary_remote_addr:$2; default $binary_remote_addr; } limit_req_zone $limit_req_zone_key zone=mylimit:10m rate50/s; limit_req_status 429; # other configs.. upstream supertokens { server localhost:3567; } server { limit_req zone=mylimit burst=50 nodelay; # other configs.. listen 0.0.0.0:80; location / { proxy_pass http://supertokens; } } } ``` In the above, the core adds a rate limit per app per IP address. # Deployment - Scalability Source: https://supertokens.com/docs/deployment/scalability ## Overview The following page addresses how the **SuperTokens** components scale based on different factors. --- ## Users and tenants SuperTokens can handle 10s of millions of users and tenants. In fact, you can even make one tenant per user and it would work well. For most operations, the database structure and queries allow partitioning based on tenants and users. As the number of tenants scales, it does not affect performance on most operations per tenant. Similarly, as the number of users scales, it does not affect performance on most operations per user. --- ## SuperTokens core :::important If you are using the managed service, the SuperTokens core is fully managed, and you don't have to worry about scaling it. This section is for those who are self-hosting the SuperTokens core service. ::: The SuperTokens core service supports horizontal scalability. This means that you can add more instances of the core service to handle more requests. The core service is also stateless, which means that you can add or remove instances without worrying about the state of the system. The core service can handle a high number of requests per second (`RPS`). The exact number of requests per second (`RPS`) that the core service can handle depends on the hardware you are using. In general, the core service can manage many requests. For example, the average latency of requests is ~40 milliseconds at 100-150 requests per second (6,000-10,000 requests per minute). The compute deployed is 6 instances of the SuperTokens core service, each on a t3. micro EC2 instance behind a round robin load balancer. The `CPU` usage of each instance is around 10%. The scale of end users that this can support is in the order of 1-2 million monthly active users, with a total user count of millions more. ### Average latency over 1 day ### Number of requests per minute over 1 day ### Performance tuning If you are facing performance issues, here are some tips to help you tune the performance of your SuperTokens setup: - If you are self-hosting the SuperTokens core, know that it is stateless and can scale horizontally. You can add more instances of the core service to handle more requests (behind a load balancer). - Check which part of the request cycle is slow. Is it the SuperTokens core responding, or is it the backend SDK APIs responding? The performance of the backend SDK API depends mainly on how you have set up your API layer (that integrates with the backend SDK) to perform. You can check which is slow by enabling debug logs in the backend SDK, and then inspecting the timestamps around the core requests. If they sum up to be much less than the total time taken for the request (from the `frontend`'s point of view), then the bottleneck is likely in the backend SDK. - If you are self-hosting the SuperTokens core, check if there are any database queries that are too slow. You can do this using debugging tools provided by the PostgreSQL database. If you find a query that's causing issues, please reach out to support. - Check that the compute used to run the backend SDK, the SuperTokens core (in case you are self-hosting it), and the database is sufficient. Using a t3.micro EC2 instance for the core should work well for even 100,000 MAUs. You can check the `CPU` and memory usage of the instances to see if they have maxed out, or if you have run out of `CPU` credits. If they are, you can consider upgrading the instances to more powerful ones. - In case you are self-hosting the SuperTokens core, you can tune its performance by setting different values for the following configurations in the configuration.yaml file, or docker `env`: - `max_server_pool_size`: Sets the max thread pool size for incoming `http` server requests. Default value is 10. - `postgresql_connection_pool_size` (if using psql): Defines the connection pool size to PostgreSQL. Default value is 10. - `postgresql_minimum_idle_connections` (if using psql): Minimum number of idle connections to remain active. If not set, minimum idle connections are the same as the connection pool size. By default, this is not set. - `postgresql_idle_connection_timeout`: (if using psql): Timeout in milliseconds for the idle connections to close. Default is 60000 MS. - Check if you have access token blacklisting enabled in the backend SDK. The default is `false`, but if you have it enabled, then it means that every session verification attempt queries the SuperTokens core to check the database. This adds latency to the session verification process and increases the load on the core. If you want to keep this to `true`, consider making it only for non `GET` APIs for your application. - You can increase the value of `access_token_validity` in the SuperTokens core. It sets the validity of the access token. Default value is 3,600 seconds (1 hour). The lower this value, the more often the refresh API calls the core, increasing the load on the core. --- ## Database SuperTokens works with PostgreSQL databases, and one instance of the database is enough to handle tens of millions of MAUs. For example, a database with 1 million users would occupy ~ 1.5 GB of disk space (assuming you add minimal custom metadata to the user object). --- ## Backend SDK The backend SDK does not store any information on its own. It's a "big middleware" between the frontend requests and the SuperTokens core. As such, its scalability depends entirely on the scalability of your API layer into which the backend SDK integrates. --- ## Session verification The access token is a JWT, and the backend SDK verifies them without any network requests, making them fast and scalable. The core service verifies the refresh token, and the scalability of session refresh requests depends on the core service's scalability. However, session refreshes are rare compared to access token verification. # Migration - Account Migration Source: https://supertokens.com/docs/migration/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 '/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 = ''; 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 BASE_URL = "" 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()) ``` Creates one user at a time based on the request body. **Authorization**: Set the `api-key` header to the value of your **SuperTokens** Core API key.

Request

Body Schema

| Name | Type | Description | Required | |----------------|------------------------|-------------------------------------------------------------------------------------------------|----------| | externalUserId | `string` | ID that can be used to reference the users from your previous provider | No | | `userMetadata` | `object` | An object with custom user information that can be used later on | No | | `userRoles` | `array` of `UserRole` | The roles that will be used for authorization | No | | `totpDevices` | `array` of `TotpDevice` | Time-based One-time Password device (TOTP) used for Multi-Factor Authentication | Yes | | `loginMethods` | `array` of `LoginMethod` | The actual authentication methods and credentials | Yes |

TotpDevice

| Name | Type | Description | Required | |------------|---------------------|-----------------------------------------------------------------------------|----------| | `secretKey` | `string` | The secret key used to generate the TOTP codes. | Yes | | period | `number` | The time period in seconds for which a TOTP code is valid. | Yes | | skew | `number` | The allowable time skew to account for clock differences between the server and device. | Yes | | `deviceName` | `string` | The name assigned to the TOTP device for identification purposes. | No |

UserRole

| Name | Type | Description | Required | |------------|---------------------|-----------------------------------------------------------------------------|----------| | role | `string` | The actual role name | Yes | | tenantIds | Array of `string` | The tenants that use this role. If you are not using the `multi-tenancy` just pass an empty array. | Yes |

LoginMethod

`LoginMethod` is a polymorphic type with multiple variants based on the `recipeId`. Each variant includes shared and specific fields. ##### EmailPassword ###### With Encrypted Password | Name | Type | Description | Required | |-------------------------|-----------------------|-------------------------------------------------------------------------------------------------|----------| | email | `string` | User's email address | Yes | | `passwordHash` | `string` | Hashed password | Yes | | `hashingAlgorithm` | `enum` (`"bcrypt"`, `"argon2"`, `"firebase_scrypt"`) | Hashing algorithm used. | Yes | | `recipeId` | `string` | Must be `emailpassword` | Yes | | `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | | `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | | tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | | externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | | timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No | ###### With Plain Password | Name | Type | Description | Required | |-------------------------|-----------------------|-------------------------------------------------------------------------------------------------|----------| | email | `string` | User's email address | Yes | | `plaintextPassword` | `string` | A plain text password that will be hashed based on the configured hashing algorithm | Yes | | `recipeId` | `string` | Must be `emailpassword` | Yes | | `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | | `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | | tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | | externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | | timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No | ##### ThirdParty | Name | Type | Description | Required | |------------------|--------|-------------------------------|----------| | `recipeId` | `string` | Must be `"thirdparty"` | Yes | | email | `string` | User's email address | Yes | | thirdPartyId | `string` | Identifier for the third party provider | Yes | | thirdPartyUserId | `string` | User identifier from the third party provider | Yes | | tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | | externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | | `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | | `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | | timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No | ##### Passwordless | Name | Type | Description | Required | |----------|--------|-----------------------|----------| | `recipeId` | `string` | Must be `"passwordless"` | Yes | | email | `string` | User's email address | One of `email` or `phoneNumber` must be provided | | `phoneNumber` | `string` | User's phone number | One of `email` or `phoneNumber` must be provided | | tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | | externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | | `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | | `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | | timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No |

Example

```bash curl --location --request POST '/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" } ] } ' ```

Response

200

The user has been successfully created.

Example

```json { "status": "OK", "user": { /* User Object */ } } ```

400

The request body was invalid.

Example

```json { "errors": ["No two loginMethods can have isPrimary as true.", "email is required for recipeId emailpassword", "hashingAlgorithm must be one of 'bcrypt', 'argon2', or 'firebase_scrypt'."]} } ```

500

An internal server error occurred.
:::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. ::: ### 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 '/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 = ''; 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 BASE_URL = "" 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()) ``` Stages users to be imported by a background cron job. **Authorization**: Set the `api-key` header to the value of your **SuperTokens** Core API key.

Request

Body Schema

| Name | Type | Description | Required | |----------------|---------------------|-----------------------------------|----------| | users | `Array` of `User` objects | The users that you want to import. The array has a limit of 10000 items. | Yes |

User

| Name | Type | Description | Required | |----------------|------------------------|-------------------------------------------------------------------------------------------------|----------| | externalUserId | `string` | ID that can be used to reference the users from your previous provider | No | | `userMetadata` | `object` | An object with custom user information that can be used later on | No | | `userRoles` | `array` of `UserRole` | The roles that will be used for authorization | No | | `totpDevices` | `array` of `TotpDevice` | Time-based One-time Password device (TOTP) used for Multi-Factor Authentication | Yes | | `loginMethods` | `array` of `LoginMethod` | The actual authentication methods and credentials | Yes | ##### TotpDevice | Name | Type | Description | Required | |------------|---------------------|-----------------------------------------------------------------------------|----------| | `secretKey` | `string` | The secret key used to generate the TOTP codes. | Yes | | period | `number` | The time period in seconds for which a TOTP code is valid. | Yes | | skew | `number` | The allowable time skew to account for clock differences between the server and device. | Yes | | `deviceName` | `string` | The name assigned to the TOTP device for identification purposes. | No |

UserRole

| Name | Type | Description | Required | |------------|---------------------|-----------------------------------------------------------------------------|----------| | role | `string` | The actual role name | Yes | | tenantIds | Array of `string` | The tenants that use this role. If you are not using the `multi-tenancy` just pass an empty array. | Yes | ##### LoginMethod `LoginMethod` is a polymorphic type with multiple variants based on the `recipeId`. Each variant includes shared and specific fields. ###### EmailPassword ###### With Encrypted Password | Name | Type | Description | Required | |-------------------------|-----------------------|-------------------------------------------------------------------------------------------------|----------| | email | `string` | User's email address | Yes | | `passwordHash` | `string` | Hashed password | Yes | | `hashingAlgorithm` | `enum` (`"bcrypt"`, `"argon2"`, `"firebase_scrypt"`) | Hashing algorithm used. | Yes | | `recipeId` | `string` | Must be `emailpassword` | Yes | | `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | | `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | | tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | | externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | | timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No | ###### With Plain Password | Name | Type | Description | Required | |-------------------------|-----------------------|-------------------------------------------------------------------------------------------------|----------| | email | `string` | User's email address | Yes | | `plaintextPassword` | `string` | A plain text password that will be hashed based on the configured hashing algorithm | Yes | | `recipeId` | `string` | Must be `emailpassword` | Yes | | `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | | `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | | tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | | externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | | timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No | ##### ThirdParty | Name | Type | Description | Required | |------------------|--------|-------------------------------|----------| | `recipeId` | `string` | Must be `"thirdparty"` | Yes | | email | `string` | User's email address | Yes | | thirdPartyId | `string` | Identifier for the third party provider | Yes | | thirdPartyUserId | `string` | User identifier from the third party provider | Yes | | tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | | externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | | `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | | `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | | timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No | ##### Passwordless | Name | Type | Description | Required | |----------|--------|-----------------------|----------| | `recipeId` | `string` | Must be `"passwordless"` | Yes | | email | `string` | User's email address | One of `email` or `phoneNumber` must be provided | | `phoneNumber` | `string` | User's phone number | One of `email` or `phoneNumber` must be provided | | tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | | externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | | `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | | `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | | timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No |

Example

```bash curl --location --request POST '/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" } ] } ] } ```

Response

200

All the users have been added to the `bulk_import_users` table.

Example

```json { "status": "OK", } ```

400

The request body was invalid.

Example

```json ```json { error: "Failed to add users for the bulk import. Please fix the error in the following users", users: [ { index: 11, errors: ["No two loginMethods can have isPrimary as true."] }, { index: 15, errors: ["email is required for recipeId emailpassword", "hashingAlgorithm must be one of 'bcrypt', 'argon2', or 'firebase_scrypt'."] } ] } ``` ##### Errors - Account linking must be enabled if more than one login method is provided for a user. - Multitenancy must be enabled if a `tenantId` is other than `public.` - No two `loginMethods` can have `isPrimary` as `true`. - Invalid `appId` or `tenantId`. - A valid `email` is required. - A valid `passwordHash` is required. - `hashingAlgorithm` must be one of 'bcrypt', 'argon2', or `firebase_scrypt`. - A valid `email` is required. - `thirdPartyUserId` is required. - A valid `email` or `phoneNumber` is required.

500

An internal server error occurred.
:::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 '/bulk-import/users/count?status=PROCESSING' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ ``` ```tsx const BASE_URL = ''; 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 ``` ```tsx const BASE_URL = ''; 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 :::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. ::: # Migration - Session Migration Source: https://supertokens.com/docs/migration/session-migration This guide will show you how to migrate your user sessions from you previous authentication provider to **SuperTokens**. ## Overview To achieve a seamless transition process, you will also have to migrate the active sessions that use your previous authentication provider to **SuperTokens**. To do this you should create a new flow that will determine if an existing user session needs to be migrated, and call the migration API if necessary. You can see a detailed illustration of the process below. Session migration flow chart ## Steps ### 1. Add the session migration endpoint Create a new endpoint on your backend that will generate a new **SuperTokens Session** based on the current authentication token. This will be called by the frontend if a user is still logged in through your previous authentication provider. ```tsx title="Backend changes" let app = express(); app.post("/migrate-session", async (req, res) => { // extract the access token from the request object if(req.headers.authorization !== undefined){ let access_token = req.headers.authorization.split("Bearer ")[1]; // verify the access token and retrieve the old userId let customUserId = await verifyAccessTokenAndRetriveUserId(access_token); // create a new SuperTokens session using the customUserId // the createNewSession function will attach the SuperTokens session tokens to the response object. // @ts-ignore await Session.createNewSession(req, res, customUserId) res.send({ status: "OK" }) } // handle access_token not present in request }) async function verifyAccessTokenAndRetriveUserId(access_token: string): Promise { // verify the access_token and return the decoded userId return "..."; } ``` :::info Important Verifying the access token will require you to access your previous providers JWKS endpoint. This means that you will have to continue to use your previous auth provider even after switching to SuperTokens. If you want to immediately stop querying your previous authentication provider you can use the JWKS public keys and provide them as a secret to the JWT verification function. You can follow our [guide](/docs/additional-verification/session-verification/protect-api-routes#with-the-public-key-string) on how to do this. ::: ### 2. Call the migration endpoint from your frontend app On your frontend, on page load, check if a session with your previous authentication provider exists. - If a session exists, send a request to the `/migrate-session` API with the access token to create a new SuperTokens session. - Revoke the old session. ```tsx title="Frontend changes" // Call this function on page load async function migrateUserSessions() { let apiDomain = "..."; // On page load retrieve the users access token if a session exists let accessToken = await getAccessTokenFromOldProvider() if (accessToken !== undefined) { // send a request to your migrate session endpoint with the bearer token await axios.post(`${apiDomain}/migrate-session`, { headers: { "Authorization": `Bearer ${accessToken}` } }) await revokeSessionFromOldProvider() } } async function getAccessTokenFromOldProvider(): Promise { // Check if a session with your your previous provider exists and return the access_token. Return undefined otherwise return "..." } async function revokeSessionFromOldProvider() { // Revoke the session associated with the previous provider } ``` --- # Migration - Legacy Method - About Source: https://supertokens.com/docs/migration/legacy/about In this guide we will be going through the process of migrating users from an external Authentication provider to SuperTokens. User migration involves 3 steps: - Account Migration - User Creation - UserId Mapping - Mark email as verified - User Data Migration - Session Migration ## Step 1. Account Migration: ### User Creation - Our first step involves creating a SuperTokens user by importing their account credentials from the previous authentication provider. - You can learn more about how to implement these changes in the [User Creation](./account-creation/user-creation) section. ### User ID Mapping - If you have stored information against existing userIds in your application table, you can use UserId Mapping to map the existing userId to the user's SuperTokens userId. - Once the userIds are mapped you can use the existing userId to interact with all of SuperTokens APIs. - You can learn more about how to implement these changes in the [User Id Mapping](./account-creation/user-id-mapping) section. ### Mark email as verified - Once a SuperTokens user has been created and their userId has been mapped, you need to mark their email as verified (if applicable) so that they do not have to go through the email verification process again. - You can learn more about how to implement these changes in [this section](./account-creation/email-verification). ## Step 2. User Data Migration - Now that your user's account has been migrated over to SuperTokens we can associate additional information like roles and metadata with your user. - You can learn more about how to implement these changes in the [User Data Migration](./data-migration) section. ## Step 3. Session Migration - If you have users with an existing session, you can use [this guide](./session-migration) to migrate their external provider sessions to a SuperTokens session. - This will prevent users from having to re-authenticate. You can learn more about how to implement these changes in the [Session Migration](./session-migration) section. ## Step 4. MFA migration If you are using MFA in your app, checkout the MFA migration section [here](/docs/additional-verification/mfa/migration/legacy-to-new) after you have gone through the above migration steps. --- # Migration - Legacy Method - Step 1: Account Creation - User Creation Source: https://supertokens.com/docs/migration/legacy/account-creation/user-creation ## Email Password Migration :::important If you do not have access to your user's password hashes, you can use our [guide for migrating them dynamically during login](./ep-migration-without-password-hash). ::: SuperTokens allows you to import users with password hashes generated with `BCrypt`, `Argon2` and `Firebase SCrypt` with our import user API. You can find the API spec [here](https://app.swaggerhub.com/apis/supertokens/CDI/2.16.0#/EmailPassword%20Recipe/userImport). ### Migrating users With Argon2 or `BCrypt` Password hashes For users with `BCrypt` or `Argon2` password hashes you can use the following curl command to import your user. ```bash curl --location --request POST '/recipe/user/passwordhash/import' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johndoe@example.com", "passwordHash": "$argon2d$v=19$m=12,t=3,p=1$NWd0eGp4ZW91b3IwMDAwMA$57jcfXF19MyiUXSjkVBpEQ" }' ``` :::important SuperTokens accepts `BCrypt` and `Argon2` hashes in standard format. When exporting password hashes from authentication providers the structure might be changed. For example, Auth0 adds an identifier to the exported password hashes which needs to be removed before importing into SuperTokens. Sample password hashes for `BCrypt` and Argon2 in standard format: - `BCrypt`: `$2a$10$GzEm3vKoAqnJCTWesRARCe/ovjt/07qjvcH9jbLUg44Fn77gMZkmm` - Argon2: `$argon2id$v=19$m=16,t=2,p=1$VG1Oa1lMbzZLbzk5azQ2Qg$kjcNNtZ/b0t/8HgXUiQ76A` ::: ### Migrating users with Firebase `SCrypt` Password hashes Importing users from Firebases requires an update to your SuperTokens core config and formatting the input password hash. #### Step 1: Retrieve your Firebase password hashing parameters from your dashboard. Firebase password hashing details modal #### Step 2: Update the SuperTokens core to use the `base64_signer_key` - ** For Managed Service ** - Edit the core configuration in the SuperTokens Managed Service Dashboard. - Set the `firebase_password_hashing_signer_key` field in the config to the `base64_signer_key` retrieved from your firebase hashing parameters. ```bash docker run \ -p 3567:3567 \ // highlight-next-line -e FIREBASE_PASSWORD_HASHING_SIGNER_KEY="gRhC3eDeQOdyEn4bMd9c6kxguWVmcIVq/HbJKnCXdWscZx0l2WbCJ1wbg==" \ -d registry.supertokens.io/supertokens/supertokens- ``` ```yaml # Add your base64_signer_key to the following in the config.yaml file. # The file path can be found by running the "supertokens --help" command firebase_password_hashing_signer_key: "gRhC3eDeQOdyEn4bMd9c6kxguWVmcIVq/HbJKnCXdWscZx0l2WbCJ1wbg==" ``` #### Step 3: SuperTokens requires firebase password hashes to be in a specific format to be parsed. For example: Your exported firebase user has the following credentials: ```json { "users": [ { "localId": "userId", "email": "johnDoe@example.com" "passwordHash": "9Y8ICWcqbzmI42DxV1jpyEjbrJPG8EQ6nI6oC32JYz+/dd7aEjI/R7jG9P5kYh8v9gyqFKaXMDzMg7eLCypbOA==", "salt": "/cj0jC1br5o4+w==", } ] } ``` The memory cost, rounds and salt separator retrieved from the password hashing config are: ```json { mem_cost: 14, rounds: 8, base64_salt_separator: "Bw==" } ``` The password hash would be the following: `$f_scrypt$9Y8ICWcqbzmI42DxV1jpyEjbrJPG8EQ6nI6oC32JYz+/dd7aEjI/R7jG9P5kYh8v9gyqFKaXMDzMg7eLCypbOA==$/cj0jC1br5o4+w==$m=14$r=8$s=Bw==` The example password hash is in the following format `$f_scrypt$$$m=$r=$s=` #### Step 4: Run the following `curl` command to import the user ```bash curl --location --request POST '/recipe/user/passwordhash/import' \ --header 'Content-Type: application/json; charset=utf-8' \ --header 'api-key: ^{coreInfo.key}' \ --data-raw '{ "email": "test@example.com", "passwordHash": "$f_scrypt$9Y8ICWcqbzmI42DxV1jpyEjbrJPG8EQ6nI6oC32JYz+/dd7aEjI/R7jG9P5kYh8v9gyqFKaXMDzMg7eLCypbOA==$/cj0jC1br5o4+w==$m=14$r=8$s=Bw==", "hashingAlgorithm": "firebase_scrypt" }' ``` ## Passwordless Migration To migrate a Passwordless user from your previous authentication provider to SuperTokens, you will first need to generate a code for the user and then call the consume code API. ### Generate passwordless code **With Email** ```bash curl --location --request POST '/recipe/signinup/code' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johndoe@example.com" }' ``` **With Phone Number** ```bash curl --location --request POST '/recipe/signinup/code' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "phoneNumber": "+14155552671" }' ``` On successfully generating the passwordless code you should see the following response ```json { "status": "OK", "preAuthSessionId": "d3Zpa9eoyV2Wr7uN5DLr6H1clzbwwGTc_0wIIXJT55M=", "codeId": "4fe93f8e-a5da-4588-82e2-314c6993b345", "deviceId": "+cWm1Y2EFxEPyHM7CAwYyAdkakBeoEDm6IOGT3xfa1U=", "userInputCode": "463152", "linkCode": "UlEb3-gbIYow61ce6RNzghkGN8qcHkpRwbhHbvMEjxY=", "timeCreated": 1664283193059, "codeLifetime": 900000 } ``` ### Consume the passwordless code to create the passwordless user Retrieve the `preAuthSessionId` and `linkCode` from the previous response and set them as request body parameters for the consume code request. ```bash curl --location --request POST '/recipe/signinup/code/consume' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "preAuthSessionId": "d3Zpa9eoyV2Wr7uN5DLr6H1clzbwwGTc_0wIIXJT55M=", "linkCode": "UlEb3-gbIYow61ce6RNzghkGN8qcHkpRwbhHbvMEjxY=" }' ``` If the user has both email and password associated with them, then you can call the update user API to associate the missing information ```bash curl --location --request PUT '/recipe/user' \ --header 'api-key: ^{coreInfo.key}' \ --header 'rid: passwordless' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "userId": "fa7a0841-b533-4478-95533-0fde890c3483", "email": "johndoe@gmail.com", "phoneNumber": "+14155552671" }' ``` ## ThirdParty Migration To migrate users with social accounts we can simply call the SuperTokens Core's `signInUp` API with the provider Id and the user's third party userId. For example: If we were importing a user with Google as their provider with their third party userId being `106347997792363870000`, we can run the following curl command to import the user. ```bash curl --location --request POST '/recipe/signinup' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "thirdPartyId": "google", "thirdPartyUserId": "106347997792363870000", "email": { "id": "johndoe@gmail.com", "isVerified": true } }' ``` --- # Migration - Legacy Method - Step 1: Account Creation - UserId Mapping Source: https://supertokens.com/docs/migration/legacy/account-creation/user-id-mapping UserId Mapping allows you to map existing userIds (from your old auth provider) to the SuperTokens userIds. This prevents you from having to update the existing `userIDs` in your application's table. As an example, if after creating the user in SuperTokens, their userId is `fa7a0841-b533-4478-95533-0fde890c3483` and the existing userId for that user is `customUserId`, then you can map these user IDs by calling the following API: ```bash curl --location --request POST '/recipe/userid/map' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "superTokensUserId": "fa7a0841-b533-4478-95533-0fde890c3483", "externalUserId": "customUserId" }' ``` Now whenever this user signs in, or if you fetch information about this user from SuperTokens, their userID will be `customUserId`. :::info Note The maximum allowed size of the `externalUserId` is 128 characters. ::: # Migration - Legacy Method - Step 1: Account Creation - Mark email as verified Source: https://supertokens.com/docs/migration/legacy/account-creation/email-verification Once a SuperTokens user has been created and their userId has been mapped, you need to mark their email as verified, if their email was verified in the old auth provider. ## Step 1. Generating the email verification token: For example with the email as `johnDoe@gmail.com` and userId as `056f4b02-c992-42ed-a8af-cb709669bbd` ```bash curl --location --request POST '/recipe/user/email/verify/token' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johnDoe@gmail.com", "userId": "056f4b02-c992-42ed-a8af-cb709669bbd" }' ``` Successfully generating an email verification token will result in the following response ```bash { "status":"OK", "token":"OWU2OGQyZWQ5MGFkMzM1M2Y4ZDMzNjE1NzA4ZGI0YWYyODEwMzg0NjJhNTcxNDZjYmY0NzJiOTZmYWE5OTJkMzRmOWVkYzBiODZkMWNmYTJkY2I5YWJkZDU2Yjg0NTU0" } ``` ## Step 2. Verifying the users email with the verification token Retrieve the token from the response of the previous request and set it in the body of the email verification request. ```bash curl --location --request POST '/recipe/user/email/verify' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "method": "token", "token": "OWU2OGQyZWQ5MGFkMzM1M2Y4ZDMzNjE1NzA4ZGI0YWYyODEwMzg0NjJhNTcxNDZjYmY0NzJiOTZmYWE5OTJkMzRmOWVkYzBiODZkMWNmYTJkY2I5YWJkZDU2Yjg0NTU0" }' ``` # Migration - Legacy Method - Step 1: Account Creation - User Creation without password hashes Source: https://supertokens.com/docs/migration/legacy/account-creation/ep-migration-without-password-hash :::caution The recommended method for migrating users to SuperTokens is by [importing users with their password hashes](./user-creation). You should only use the following method if you do not have access to your user's password hashes and still have access to your previous identity provider. ::: SuperTokens also supports the "**in time**" user migration strategy for when password hashes cannot be exported from your legacy provider. We need to make the following customizations to SuperTokens authentication flows to support this strategy: - **Step 1) Prevent sign ups from users who exist in the external provider.** - To prevent duplicate accounts from being created, we block sign ups from users who have existing accounts with the external provider. - **Step 2) Create a SuperTokens account for users trying to sign in if they have an account with the external provider.** - We modify the sign in flow to check if the user signing in has an existing account with the external provider and not with SuperTokens. If their input credentials are valid, we create a SuperTokens user and import their user data. - **Step 3) Create a SuperTokens account for users who have an account with the external provider but have forgotten their password.** - Some users who have an account with the external provider and not with SuperTokens may have forgotten their passwords and trigger the password reset flow. Since SuperTokens requires an existing account to send the reset password email to, we need to modify the password reset flow to check that if the user needs to be migrated. If they do, we create a SuperTokens account with a temporary password, import their user data and continue the password reset flow. - To ensure that users can only sign in once they successfully reset their passwords we add the `isUsingTemporaryPassword` flag to the account's metadata. We also modify the sign in flow to block sign ins from accounts with this metadata. - **Step 4) Remove the `isUsingTemporaryPassword` flag on successful password reset** - Once the password has been successfully reset we check if the user has the `isUsingTemporaryPassword` flag set in the metadata. If they do we clear the flag from the user's metadata. - **Step 5) Update the login flow to account for the `isUsingTemporaryPassword` flag** - We also update the login flow to prevent sign ins from accounts who have the `isUsingTemporaryPassword` flag and if their input password does not match the one in the legacy auth provider. This ensures that users who started the password reset flow are forced to finish it. ## Step 1) Prevent sign ups from users who exist in the external provider To implement this change we override the API that handles email-password login when initializing the recipe on the backend. ```tsx EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, signUpPOST: async function (input) { let email = input.formFields.find((field) => field.id === "email")!.value as string; // Check if the user signing in exists in the external provider if (await doesUserExistInExternalProvider(email)) { // Return status "EMAIL_ALREADY_EXISTS_ERROR" since the user already exists in the external provider return { status: "EMAIL_ALREADY_EXISTS_ERROR" } } return originalImplementation.signUpPOST!(input); }, } }, } }) async function doesUserExistInExternalProvider(email: string): Promise { // TODO: check if user with the input email exists in the external provider return false; } ``` ```python from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, EmailAlreadyExistsError, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.session.interfaces import SessionContainer def override_email_password_apis(original_implementation: APIInterface): original_sign_up = original_implementation.sign_up_post async def sign_up( form_fields: List[FormField], tenant_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], api_options: APIOptions, user_context: Dict[str, Any], ): email = "" for field in form_fields: if field.id == "email": email = field.value # check if the user signing in exists in the external provider if await does_user_exist_in_external_provider(email): # Return SignUpEmailAlreadyExistsError since the user exists in the external provider return EmailAlreadyExistsError() return await original_sign_up( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) original_implementation.sign_up_post = sign_up return original_implementation async def does_user_exist_in_external_provider(email: str): # TODO: Check if a user with the input email exists in the external provider return False init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig( apis=override_email_password_apis, ) ) ], ) ``` ```go EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, signInPOST: async function (input) { // Check if an email-password user with the input email exists in SuperTokens let email = input.formFields.find((field) => field.id === "email")!.value as string; let password = input.formFields.find((field) => field.id === "password")!.value as string; let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { email: email }, undefined, input.userContext); let emailPasswordUser = supertokensUsersWithSameEmail.find(u => { return u.loginMethods.find(lM => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined; }) if (emailPasswordUser === undefined) { // EmailPassword user with the input email does not exist in SuperTokens // Check if the input credentials are valid in the external provider let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password) if (legacyUserInfo === undefined) { // credentials are incorrect return { status: "WRONG_CREDENTIALS_ERROR" } } // Call the signup function to create a new SuperTokens user. let signUpResponse = await EmailPassword.signUp(input.tenantId, email, password, undefined, input.userContext); if (signUpResponse.status !== "OK") { throw new Error("Should never come here") } // Map the external provider's userId to the SuperTokens userId await SuperTokens.createUserIdMapping({ superTokensUserId: signUpResponse.user.id, externalUserId: legacyUserInfo.user_id }) // Set the userId in the response to use the provider's userId signUpResponse.user.id = legacyUserInfo.user_id signUpResponse.user.loginMethods[0].recipeUserId = new RecipeUserId(legacyUserInfo.user_id); signUpResponse.recipeUserId = new RecipeUserId(legacyUserInfo.user_id); // We also need to set the email verification status of the user if (legacyUserInfo.isEmailVerified) { // Generate an email verification token for the user let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signUpResponse.recipeUserId, email, input.userContext); if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext); } } } return originalImplementation.signInPOST!(input) }, } }, } }) async function validateAndGetUserInfoFromExternalProvider(email: string, password: string): Promise<{ user_id: string, isEmailVerified: boolean } | undefined> { // TODO: Validate the input credentials against the external authentication provider. If the credentials are valid return the user info. return undefined } ``` ```python from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import ( create_user_id_mapping, list_users_by_account_info, ) from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.asyncio import sign_up from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, SignUpOkResult, WrongCredentialsError, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.emailverification.asyncio import ( create_email_verification_token, verify_email_using_token, ) from supertokens_python.recipe.emailverification.interfaces import ( CreateEmailVerificationTokenOkResult, ) from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.types import RecipeUserId from supertokens_python.types.base import AccountInfoInput def override_emailpassword_apis(original_implementation: APIInterface): original_emailpassword_sign_in = original_implementation.sign_in_post async def sign_in( form_fields: List[FormField], tenant_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], api_options: APIOptions, user_context: Dict[str, Any], ): email = "" password = "" for field in form_fields: if field.id == "email": email = field.value if field.id == "password": password = field.value # Check if an email-password user with the input email exists in SuperTokens supertokens_user_with_same_email = await list_users_by_account_info( tenant_id, AccountInfoInput(email=email), False, user_context ) emailpassword_user = next( ( user for user in supertokens_user_with_same_email if any( lm.recipe_id == "emailpassword" and lm.has_same_email_as(email) for lm in user.login_methods ) ), None, ) if emailpassword_user is None: # EmailPassword user with the input email does not exist in SuperTokens # Check if the input credentials valid in the external provider legacy_user_info = await validate_and_get_user_info_from_external_provider( email, password ) if legacy_user_info is None: # Credentials are incorrect return WrongCredentialsError() # Call the sign_up function to create a new SuperTokens user. response = await sign_up(tenant_id, email, password, None, user_context) if not isinstance(response, SignUpOkResult): raise Exception("Should never come here") # Map the external provider's userId to the SuperTokens userId await create_user_id_mapping(response.user.id, legacy_user_info.user_id) # Set the userId in the response to use the provider's userId response.user.id = legacy_user_info.user_id response.user.login_methods[0].recipe_user_id = RecipeUserId( legacy_user_info.user_id ) response.recipe_user_id = RecipeUserId(legacy_user_info.user_id) # We also need to set the email verification status of the user if legacy_user_info.isEmailVerified: # Generate an email verification token for the user generate_email_verification_response = ( await create_email_verification_token( tenant_id, response.recipe_user_id, email, user_context, ) ) if isinstance( generate_email_verification_response, CreateEmailVerificationTokenOkResult, ): await verify_email_using_token( tenant_id, generate_email_verification_response.token, True, user_context, ) return await original_emailpassword_sign_in( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) original_implementation.sign_in_post = sign_in return original_implementation class ExternalUserInfo: def __init__(self, user_id: str, isEmailVerified: bool): self.user_id: str = user_id self.isEmailVerified: bool = isEmailVerified async def validate_and_get_user_info_from_external_provider( email: str, password: str ) -> Union[None, ExternalUserInfo]: return None init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) ) ], ) ``` ```go EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, // Add overrides from the previous step generatePasswordResetTokenPOST: async (input) => { // Retrieve the email from the input let email = input.formFields.find(i => i.id === "email")!.value as string; // check if user exists in SuperTokens let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { email }, undefined, input.userContext); let emailPasswordUser = supertokensUsersWithSameEmail.find(u => { return u.loginMethods.find(lM => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined; }) if (emailPasswordUser === undefined) { // User does not exist in SuperTokens // Check if the user exists in the legacy provider and retrieve their data let legacyUserData = await retrieveUserDataFromExternalProvider(email); if (legacyUserData) { // create a SuperTokens account for the user with a temporary password let tempPassword = await generatePassword(); let signupResponse = await EmailPassword.signUp(input.tenantId, email, tempPassword, undefined, input.userContext); if (signupResponse.status === "OK") { // If user is succesfully created we map the legacy id to their SuperTokens Id. await SuperTokens.createUserIdMapping({ superTokensUserId: signupResponse.user.id, externalUserId: legacyUserData.user_id }) signupResponse.user.id = legacyUserData.user_id signupResponse.user.loginMethods[0].recipeUserId = new RecipeUserId(legacyUserData.user_id); signupResponse.recipeUserId = new RecipeUserId(legacyUserData.user_id); // We also need to set the email verification status of the user if (legacyUserData.isEmailVerified) { // Generate an email verification token for the user let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signupResponse.recipeUserId, email, input.userContext); if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext); } } // We also need to identify that the user is using a temporary password. We do through the userMetadata recipe UserMetadata.updateUserMetadata(legacyUserData.user_id, { isUsingTemporaryPassword: true }) } else { throw new Error("Should never come here") } } } return await originalImplementation.generatePasswordResetTokenPOST!(input); }, } } } }) async function generatePassword(): Promise { // TODO: generate a random password return "" } async function retrieveUserDataFromExternalProvider(email: string): Promise<{ user_id: string, isEmailVerified: boolean } | undefined> { // TODO: retrieve user data if a user with the input email exists in the external provider. return undefined; } ``` ```python from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import ( create_user_id_mapping, list_users_by_account_info, ) from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.asyncio import sign_up from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, SignUpOkResult, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.emailverification.asyncio import ( create_email_verification_token, verify_email_using_token, ) from supertokens_python.recipe.emailverification.interfaces import ( CreateEmailVerificationTokenOkResult, ) from supertokens_python.recipe.usermetadata.asyncio import update_user_metadata from supertokens_python.types import RecipeUserId from supertokens_python.types.base import AccountInfoInput def override_emailpassword_apis(original_implementation: APIInterface): original_generate_password_reset_token_post = ( original_implementation.generate_password_reset_token_post ) async def generate_password_reset_token_post( form_fields: List[FormField], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): # retrieve the email from the form fields email = None for field in form_fields: if field.id is "email": email = field.value if email is None: raise Exception("Should never come here") # Check if an email-password user with the input email exists in SuperTokens supertokens_user_with_same_email = await list_users_by_account_info( tenant_id, AccountInfoInput(email=email), False, user_context ) emailpassword_user = next( ( user for user in supertokens_user_with_same_email if any( lm.recipe_id == "emailpassword" and lm.has_same_email_as(email) for lm in user.login_methods ) ), None, ) if emailpassword_user is None: # EmailPassword user with the input email does not exist in SuperTokens # Check if the user exists in the legacy provider and retrieve their data legacy_user_data = await retrieve_user_data_from_external_provider(email) if legacy_user_data is not None: # Create a SuperTokens account for the user with a temporary password tempPassword = await generate_password() response = await sign_up( tenant_id, email, tempPassword, None, user_context ) if not isinstance(response, SignUpOkResult): raise Exception("Should never come here") # Map the SuperTokens userId to the legacy userId await create_user_id_mapping(response.user.id, legacy_user_data.user_id) response.user.id = legacy_user_data.user_id response.user.login_methods[0].recipe_user_id = RecipeUserId( legacy_user_data.user_id ) response.recipe_user_id = RecipeUserId(legacy_user_data.user_id) # We also need to set the email verification status if legacy_user_data.isEmailVerified: # Generate an email verification token for the user generate_email_verification_token_response = ( await create_email_verification_token( tenant_id, response.recipe_user_id, email, user_context ) ) if isinstance( generate_email_verification_token_response, CreateEmailVerificationTokenOkResult, ): # Verify the user's email await verify_email_using_token( tenant_id, generate_email_verification_token_response.token, True, user_context, ) # We also need to identify that the user is using a temporary password. We do through the userMetadata recipe await update_user_metadata( legacy_user_data.user_id, {"isUsingTemporaryPassword": True} ) return await original_generate_password_reset_token_post( form_fields, tenant_id, api_options, user_context ) original_implementation.generate_password_reset_token_post = ( generate_password_reset_token_post ) return original_implementation class ExternalUserInfo: def __init__(self, user_id: str, isEmailVerified: bool): self.user_id: str = user_id self.isEmailVerified: bool = isEmailVerified async def retrieve_user_data_from_external_provider( email: str, ) -> Union[None, ExternalUserInfo]: # TODO: Retrieve user data if a user with the input email exists in the external provider. return None async def generate_password(): # TODO: generate a random password return "" init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) ) ], ) ``` ```go var email *string = nil for _, field := range formFields { if field.ID == "email" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } email = &valueAsString } } if email == nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("Should never come here") } // Check if an email-password user with the input email exists in SuperTokens emailPasswordUser, err := emailpassword.GetUserByEmail(tenantId, *email) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if emailPasswordUser == nil { // User does not exist in SuperTokens // Check if the user exists in the legacy provider and retrieve their data legacyUserInfo := retrieveUserDataFromExternalProvider(*email) if legacyUserInfo != nil { // Create a SuperTokens account for the user with a temporary password tempPassword := generatePassword() response, err := emailpassword.SignUp(tenantId, *email, tempPassword) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if response.OK == nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("should never come here") } // Map the external provider's userId to the SuperTokens userId _, err = supertokens.CreateUserIdMapping(response.OK.User.ID, legacyUserInfo.userId, nil, nil) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } // Set the userId in the response to use the provider's userId response.OK.User.ID = legacyUserInfo.userId // We also need to set the email verification status of the user if legacyUserInfo.isEmailVerified { generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, response.OK.User.ID, &response.OK.User.Email) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if generateEmailVerificationTokenResponse.OK != nil { // Verify the user's email _, err := emailverification.VerifyEmailUsingToken(tenantId, generateEmailVerificationTokenResponse.OK.Token) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } } } // We also need to identify that the user is using a temporary password. We do through the UserMetadata recipe _, err = usermetadata.UpdateUserMetadata(legacyUserInfo.userId, map[string]interface{}{ "isUsingTemporaryPassword": true, }) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } } } return originalGeneratePasswordResetTokenPOST(formFields, tenantId, options, userContext) } return originalImplementation }, }, }), usermetadata.Init(nil), }, }) } type ExternalUserInfo struct { userId string isEmailVerified bool } func retrieveUserDataFromExternalProvider(email string) *ExternalUserInfo { // TODO: Retrieve user info from external provider if account with input email exists. return nil } func generatePassword() string { // TODO: generate a random password return "" } ``` The code above overrides the `generatePasswordResetTokenPOST` API. This is the first step in the password reset flow and is responsible for generating the password reset token to be sent with the reset password email. - Similar to the previous step, we need to determine whether to migrate the user or not. - The next step is to create a SuperTokens account with a temporary password, the password can be a random string since it is reset by the user when they complete the reset password flow. - We now map the external `userId`(the userId from the external provider) to the SuperTokens `userId`. This allows SuperTokens functions to reference the user with the external `userId`. - Depending on the email verification status of the user in the external provider we also verify the user's email in SuperTokens. - We assign the `isUsingTemporaryPassword` flag to user's metadata since the account was generated with a temporary password. This is done to prevent sign ins until the password is successfully reset. ## Step 4) Remove the `isUsingTemporaryPassword` flag on successful password reset If the password reset flow is successfully completed we need to check if the user has `isUsingTemporaryPassword` set in their metadata and remove it if it exists. ```tsx EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, // TODO: implementation details in previous step passwordResetPOST: async function (input) { let response = await originalImplementation.passwordResetPOST!(input); if (response.status === "OK") { let usermetadata = await UserMetadata.getUserMetadata(response.user.id, input.userContext) if (usermetadata.status === "OK" && usermetadata.metadata.isUsingTemporaryPassword) { // Since the password reset we can remove the isUsingTemporaryPassword flag await UserMetadata.updateUserMetadata(response.user.id, { isUsingTemporaryPassword: null }) } } return response } } } } }) ``` ```python from typing import Any, Dict, List from supertokens_python import InputAppInfo, init from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, PasswordResetPostOkResult, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.usermetadata.asyncio import ( get_user_metadata, update_user_metadata, ) def override_emailpassword_apis(original_implementation: APIInterface): original_password_reset_post = original_implementation.password_reset_post async def password_reset_post( form_fields: List[FormField], token: str, tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): response = await original_password_reset_post( form_fields, token, tenant_id, api_options, user_context ) if ( isinstance(response, PasswordResetPostOkResult) ): # Check that the user has the isUsingTemporaryPassword flag set in their metadata metadata_result = await get_user_metadata(response.user.id, user_context) if ( "isUsingTemporaryPassword" in metadata_result.metadata and metadata_result.metadata["isUsingTemporaryPassword"] is True ): # Since the password has been successfully reset, we can remove the isUsingTemporaryPassword flag await update_user_metadata( response.user.id, {"isUsingTemporaryPassword": None} ) return response original_implementation.password_reset_post = password_reset_post return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) ) ], ) ``` ```go - Prevent sign in from accounts that have temporary passwords. - If, for any reason, the user tries to sign into their account with the temporary password, then the login method should be blocked. - If a user initiates a password reset but remembers their password, they should be able to sign in. - In this case the user should be able to login and the database should be updated to reflect the new password. ```tsx EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, signInPOST: async function (input) { // Check if an email-password user with the input email exists in SuperTokens let email = input.formFields.find((field) => field.id === "email")!.value as string; let password = input.formFields.find((field) => field.id === "password")!.value as string; let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { email: email }, undefined, input.userContext); let emailPasswordUser = supertokensUsersWithSameEmail.find(u => { return u.loginMethods.find(lM => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined; }) if (emailPasswordUser === undefined) { // EmailPassword user with the input email does not exist in SuperTokens // Check if the input credentials are valid in the external provider let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password) if (legacyUserInfo === undefined) { // credentials are incorrect return { status: "WRONG_CREDENTIALS_ERROR" } } // Call the signup function to create a new SuperTokens user. let signUpResponse = await EmailPassword.signUp(input.tenantId, email, password, undefined, input.userContext); if (signUpResponse.status !== "OK") { throw new Error("Should never come here") } // Map the external provider's userId to the SuperTokens userId await SuperTokens.createUserIdMapping({ superTokensUserId: signUpResponse.user.id, externalUserId: legacyUserInfo.user_id }) // Set the userId in the response to use the provider's userId signUpResponse.user.id = legacyUserInfo.user_id signUpResponse.user.loginMethods[0].recipeUserId = new RecipeUserId(legacyUserInfo.user_id); // We also need to set the email verification status of the user if (legacyUserInfo.isEmailVerified) { // Generate an email verification token for the user let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signUpResponse.recipeUserId, email, input.userContext); if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext); } } emailPasswordUser = signUpResponse.user; } // highlight-start // Check if the user signing in has a temporary password let userMetadata = await UserMetadata.getUserMetadata(emailPasswordUser.id, input.userContext) if (userMetadata.status === "OK" && userMetadata.metadata.isUsingTemporaryPassword) { // Check if the input credentials are valid in the external provider let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password); if (legacyUserInfo) { let loginMethod = emailPasswordUser.loginMethods.find(lM => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(email)); // Update the user's password with the correct password EmailPassword.updateEmailOrPassword({ recipeUserId: loginMethod!.recipeUserId, password: password, applyPasswordPolicy: false }) // Update the user's metadata to remove the isUsingTemporaryPassword flag UserMetadata.updateUserMetadata(emailPasswordUser.id, { isUsingTemporaryPassword: null }) } else { return { status: "WRONG_CREDENTIALS_ERROR" } } } // highlight-end return originalImplementation.signInPOST!(input) }, } }, } }) async function validateAndGetUserInfoFromExternalProvider(email: string, password: string): Promise<{ user_id: string, isEmailVerified: boolean } | undefined> { // TODO: Validate the input credentials against the external authentication provider. If the credentials are valid return the user info. return undefined } ``` ```python from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import ( create_user_id_mapping, list_users_by_account_info, ) from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.asyncio import ( sign_up, update_email_or_password, ) from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, SignUpOkResult, WrongCredentialsError, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.emailverification.asyncio import ( create_email_verification_token, verify_email_using_token, ) from supertokens_python.recipe.emailverification.interfaces import ( CreateEmailVerificationTokenOkResult, ) from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.usermetadata.asyncio import ( get_user_metadata, update_user_metadata, ) from supertokens_python.types import RecipeUserId from supertokens_python.types.base import AccountInfoInput def override_emailpassword_apis(original_implementation: APIInterface): original_emailpassword_sign_in = original_implementation.sign_in_post async def sign_in( form_fields: List[FormField], tenant_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], api_options: APIOptions, user_context: Dict[str, Any], ): email = "" password = "" for field in form_fields: if field.id == "email": email = field.value if field.id == "password": password = field.value # Check if an email-password user with the input email exists in SuperTokens supertokens_user_with_same_email = await list_users_by_account_info( tenant_id, AccountInfoInput(email=email), False, user_context ) emailpassword_user = next( ( user for user in supertokens_user_with_same_email if any( lm.recipe_id == "emailpassword" and lm.has_same_email_as(email) for lm in user.login_methods ) ), None, ) if emailpassword_user is None: # EmailPassword user with the input email does not exist in SuperTokens # Check if the input credentials valid in the external provider legacy_user_info = await validate_and_get_user_info_from_external_provider( email, password ) if legacy_user_info is None: # Credentials are incorrect return WrongCredentialsError() # Call the sign_up function to create a new SuperTokens user. response = await sign_up(tenant_id, email, password, None, user_context) if not isinstance(response, SignUpOkResult): raise Exception("Should never come here") # Map the external provider's userId to the SuperTokens userId await create_user_id_mapping(response.user.id, legacy_user_info.user_id) # Set the userId in the response to use the provider's userId response.user.id = legacy_user_info.user_id response.user.login_methods[0].recipe_user_id = RecipeUserId( legacy_user_info.user_id ) response.recipe_user_id = RecipeUserId(legacy_user_info.user_id) # We also need to set the email verification status of the user if legacy_user_info.isEmailVerified: # Generate an email verification token for the user generate_email_verification_response = ( await create_email_verification_token( tenant_id, response.recipe_user_id, email, user_context, ) ) if isinstance( generate_email_verification_response, CreateEmailVerificationTokenOkResult, ): await verify_email_using_token( tenant_id, generate_email_verification_response.token, True, user_context, ) emailpassword_user = response.user # highlight-start # Check if the user signing in has a temporary password metadata_result = await get_user_metadata(emailpassword_user.id) if ( "isUsingTemporaryPassword" in metadata_result.metadata and metadata_result.metadata["isUsingTemporaryPassword"] is True ): # Check if the input credentials are valid in the external provider legacy_user_info = await validate_and_get_user_info_from_external_provider( email, password ) if legacy_user_info is not None: # Find the emailpassword login method for the user login_method = next( ( lm for lm in emailpassword_user.login_methods if lm.recipe_id == "emailpassword" and lm.email == email ), None, ) assert login_method is not None # Update the user's password with the correct password await update_email_or_password( login_method.recipe_user_id, None, password, False, tenant_id, user_context, ) # Update the user's metadata to remove the isUsingTemporaryPassword flag await update_user_metadata( emailpassword_user.id, {"isUsingTemporaryPassword": None} ) else: return WrongCredentialsError() # highlight-end return await original_emailpassword_sign_in( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) original_implementation.sign_in_post = sign_in return original_implementation class ExternalUserInfo: def __init__(self, user_id: str, isEmailVerified: bool): self.user_id: str = user_id self.isEmailVerified: bool = isEmailVerified async def validate_and_get_user_info_from_external_provider( email: str, password: str ) -> Union[None, ExternalUserInfo]: return None init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) ) ], ) ``` ```go # Migration - Legacy Method - User Data Migration Source: https://supertokens.com/docs/migration/legacy/data-migration Once your user accounts have been migrated over to SuperTokens, additional information like metadata, roles and permissions can be associated with the user. ## User Metadata Migration SuperTokens allows you to store arbitrary data that is JSON serializable against a userId. In this example we want to store the following metadata against our user: ```json { "someKey": "someValue" } ``` ```bash curl --location --request PUT '/recipe/user/metadata' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "userId": "5acb2dbc-04f0-4c80-822a-7f06cd658f6f", "metadataUpdate": { "someKey": "someValue" } }' ``` ## User Roles Migration SuperTokens allows you to assign roles and permissions to a userId. In this example we will be assigning the `admin` role to a user: ```bash curl --location --request PUT '/recipe/user/role' \ --header 'api-key: ^{coreInfo.key}' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "role": "admin", "userId": "5acb2dbc-04f0-4c80-822a-7f06cd658f6f" }' ``` :::info Important Roles and permissions must be created before they can be assigned to a user. You can follow this [guide](/docs/additional-verification/user-roles/initial-setup) on creating roles and permissions in SuperTokens. ::: --- # Migration - Legacy Method - Session Migration Source: https://supertokens.com/docs/migration/legacy/session-migration In this section we will go over how to migrate your user sessions from you previous authentication provider to SuperTokens. This process involves two steps. - Adding a new `/migrate-session` API to your backend which will create a new SuperTokens session - Calling the `/migrate-session` API on your frontend to create a new SuperTokens session and revoke your old session. ## Flow Session migration flow chart ### Backend Changes: - Create an API on your backend, this will be called by the frontend to migrate a user's existing session to a SuperTokens session: ```tsx title="Backend changes" let app = express(); app.post("/migrate-session", async (req, res) => { // extract the access token from the request object if(req.headers.authorization !== undefined){ let access_token = req.headers.authorization.split("Bearer ")[1]; // verify the access token and retrieve the old userId let customUserId = await verifyAccessTokenAndRetriveUserId(access_token); // create a new SuperTokens session using the customUserId // the createNewSession function will attach the SuperTokens session tokens to the response object. // @ts-ignore await Session.createNewSession(req, res, customUserId) res.send({ status: "OK" }) } // handle access_token not present in request }) async function verifyAccessTokenAndRetriveUserId(access_token: string): Promise { // verify the access_token and return the decoded userId return "..."; } ``` :::info Important Verifying the access token will require you to access your previous providers JWKS endpoint, which means that you will have to continue to use your previous auth provider even after switching to SuperTokens. If you want to immediately stop querying your previous authentication provider you can use the JWKS public keys and provide them as a secret to the JWT verification function. You can follow our [guide](/docs/additional-verification/session-verification/protect-api-routes) on how to do this. ::: ### Frontend Changes: - On your frontend, on page load, check if a session with your previous authentication provider exists. - If a session exists send a request to the `/migrate-session` API with the access token to create a new SuperTokens session. - Revoke the old session. ```tsx title="Frontend changes" // Call this function on page load async function migrateUserSessions() { let apiDomain = "..."; // On page load retrieve the users access token if a session exists let accessToken = await getAccessTokenFromOldProvider() if (accessToken !== undefined) { // send a request to your migrate session endpoint with the bearer token await axios.post(`${apiDomain}/migrate-session`, { headers: { "Authorization": `Bearer ${accessToken}` } }) await revokeSessionFromOldProvider() } } async function getAccessTokenFromOldProvider(): Promise { // Check if a session with your your previous provider exists and return the access_token. Return undefined otherwise return "..." } async function revokeSessionFromOldProvider() { // Revoke the session associated with the previous provider } ``` --- # Migration - Legacy Method - MFA migration Source: https://supertokens.com/docs/migration/legacy/mfa-migration If you are using MFA in your app, checkout the MFA migration section [here](/docs/additional-verification/mfa/migration/legacy-to-new) after you have gone through the previous steps in migration. # Platform Configuration - SuperTokens Core - Add API keys Source: https://supertokens.com/docs/platform-configuration/supertokens-core/api-keys ## Overview The backend SDK uses API keys to authenticate requests to the SuperTokens core. By default, there is no API key required. If you add an API key to the core's configuration or use the managed service, you need to add it to your backend SDK code. Otherwise, the core throws a `401` error. ## Before you start :::caution no-title This page is only relevant if you are self hosting SuperTokens. ::: ## Steps ### 1. Add the key to the core instance You can set the API by updating the instance parameters. ```bash docker run \ -p 3567:3567 \ -e API_KEYS= \ -d registry.supertokens.io/supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command api_keys: ``` - The format of the value is `key1,key2,key3`. - Keys can only contain `=`, `-` and alpha-numeric (including capital) chars. - Each key must have a minimum length of 20 chars - An example value is `"Akjnv3iunvsoi8=-sackjij3ncisds,asnj9=asdcda-OI982JIUN=-a"`. Notice the `,` in the string which separates the two keys `"Akjnv3iunvsoi8=-sackjij3ncisds"` and `"asnj9=asdcda-OI982JIUN=-a"`. In the backend SDK, you should only provide one of these keys. :::info The reason for having multiple API keys is that it allows for key rotation to occur gradually if you have multiple backend systems querying the core. ::: ### 2. Add the key to your backend code Update the backend SDK initialization code to include the API key. ```tsx supertokens.init({ supertokens: { connectionURI: "", // highlight-next-line apiKey: "" }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [] }); ``` ```go # Platform Configuration - SuperTokens Core - Filter requests based on IP address Source: https://supertokens.com/docs/platform-configuration/supertokens-core/ip-allow-deny ## Overview You can set the SuperTokens core's configuration such that it accepts / denies requests that originate from certain IPs. This ensures that only your backend can query the SuperTokens core - increasing the security. ## Before you start :::caution no-title This page is only relevant if you are self hosting SuperTokens. The option is not available if you are using the managed version of SuperTokens due to security reasons. In this case, you have to configure the filtering mechanism in your backend server. ::: --- ## Allow requests ```bash docker run \ -p 3567:3567 \ -e IP_ALLOW_REGEX="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" \ -d registry.supertokens.io/supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command ip_allow_regex: 127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1 ``` The above only allows requests that originate from an IP that matches `127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1` regular expression. A breakdown of this regex is: - `127\.\d+\.\d+\.\d+`: IPs that start with `127.`; OR - `::1`: IPv6 from localhost; OR - `0:0:0:0:0:0:0:1`: IPv6 from localhost In this way, only requests from localhost are allowed, and for other requests, the core returns a `403` status code. If instead you want to allow a list of IP addresses that correspond to your backend server's IP address, you can set this value to `IP1|IP2|IP3...` - for example: `100.12.12.3|192.167.4.3|50.32.5.1`. If this value is not set, then the core allows requests from any IP address. --- ## Deny requests This is the opposite of the above configuration. If you only set this, the core allows requests from any IP other than the one that matches the regular expression corresponding to this setting. ```bash docker run \ -p 3567:3567 \ // highlight-next-line -e IP_DENY_REGEX="100.1.1.3" \ -d registry.supertokens.io/supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command ip_deny_regex: 100.1.1.3 ``` The above setting makes the core accept requests from any IP other than `100.1.1.3`. For `100.1.1.3`, it returns a `403`. :::info What if you set both the configurations? In this case, the core allows requests only based on the value of `ip_allow_regex`, as long that request's IP doesn't match the regex of `ip_deny_regex`. For example, if you set `ip_allow_regex: IP1|IP2` and `ip_deny_regex: IP1`, then the core accepts requests only from `IP2`. ::: # Platform Configuration - SuperTokens Core - Add SSL via nginx Source: https://supertokens.com/docs/platform-configuration/supertokens-core/add-ssl-via-nginx ## Overview This section guides you through setting up SSL via Nginx to query the SuperTokens Core with a secure connection. ## Before you start :::caution no-title This page is only relevant if you are self hosting SuperTokens. ::: This guide assumes you have already installed Nginx on your server. ## Steps The following example guide runs SuperTokens `localhost:3567` ### 1. Reverse proxy the SuperTokens core with nginx The SuperTokens core does not support SSL, and Nginx is needed as a reverse proxy to set up a secure connection. Start by opening the default Nginx site configuration file in a code editor. This file resides at: - Linux: `/etc/nginx/sites-available/default`. - Mac: `/usr/local/etc/nginx/sites-available/default`. - Windows: `C:\nginx\conf\nginx.conf`. In the configuration, scroll down to the `server` directive. - By default it should look like this: ```text title="/etc/nginx/sites-available/default" server { listen 80; server_name localhost; ... } ``` - Configure the `server` directive by adding the `location` directive with the following values: ```text title="/etc/nginx/sites-available/default" server { listen 80; server_name localhost; // highlight-start location / { proxy_pass http://localhost:3567; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } // highlight-end } ``` The `location` directive tells Nginx what to do with the incoming request, `proxy_pass` points the redirect to `localhost:3567`. - Test and apply the changes to Nginx by running the following command: ```bash nginx -t && service nginx restart ``` We can use the `/hello` API of the SuperTokens core to test the connection. Navigate to `http://localhost/hello` and check if it gives a valid response from the core. ### 2. Set up SSL Obtain a digital certificate to enable a secure connection with a user's browser. Self-signed certificates will be used since development is local. However, certificate authorities like [Let's Encrypt](https://letsencrypt.org/) can also generate valid certificates. - Run the following command to generate a self signed certificate using OpenSSL: ```bash openssl req -x509 -nodes -newkey rsa:2048 -keyout /etc/nginx/ssl/server.key -out /etc/nginx/ssl/server.crt ``` - Set the values `ssl_certificate` and `ssl_certificate_key` in the Nginx configuration to specify the locations of the newly generated certificates. ```text title="/etc/nginx/sites-available/default" server { listen 80; listen 443 ssl; server_name localhost; // highlight-start ssl_certificate /etc/nginx/ssl/server.crt; ssl_certificate_key /etc/nginx/ssl/server.key; // highlight-end location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } } ``` - Run the test and restart commands to test and apply your changes: ```bash nginx -t && service nginx restart ``` # Platform Configuration - SuperTokens Core - Adding a base path Source: https://supertokens.com/docs/platform-configuration/supertokens-core/base-path ## Overview If you cannot add a dedicated (sub) domain for the core and want to expose it to an external network, you may need to add a base path to all the core APIs. To do this, you have to make changes to the core's configuration as well as to the backend SDK's `init` function call. Consider an example where the core resides on `http://localhost:3567/some-prefix`. This implies that all APIs exposed by the core are on `http://localhost:3567/some-prefix/*`. ## Before you start :::caution no-title This page is only relevant if you are self hosting SuperTokens. ::: The feature is only available for core versions `>= 3.9` ## Steps ### 1. Change the core configuration ```bash docker run \ -p 3567:3567 \ // highlight-next-line -e BASE_PATH="/some-prefix" \ -d registry.supertokens.io/supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command base_path: "/some-prefix" ``` ### 2. Change the backend SDK initialization ```tsx supertokens.init({ supertokens: { // highlight-next-line connectionURI: "http://localhost:3567/some-prefix", // ... }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [/* ... */ ] }); ``` ```go ::: # Platform Configuration - SuperTokens Core - SuperTokens CLI Source: https://supertokens.com/docs/platform-configuration/supertokens-core/cli ## Overview The SuperTokens CLI has the allows you to manage your core instance from the command line. ```bash supertokens [command] [--help] [--version] ``` :::important If you are using Windows, you can only use the SuperTokens CLI using a terminal with Administrator privilege. ::: ## Commands ### Start Start an instance of SuperTokens. By default, the process starts as a daemon. ```bash supertokens start [options] ``` #### Options | Option | Description | Example | |--------|-------------|---------| | `--with-space` | Sets the amount of space, in MB, to allocate to the `JVM`. | `supertokens start --with-space=200` allocates 200MB for the `JVM` | | `--with-config` | Specify the location of the configuration file to load. Can be either relative or absolute. | `supertokens start --with-config=/usr/config.yaml` | | `--port` | Sets the port on which this instance of SuperTokens should run. | `supertokens start --port=8080` | | `--host` | Sets the host on which this instance of SuperTokens should run. | `supertokens start --host=192.168.0.1` | | `--foreground` | Runs this instance of SuperTokens in the foreground (not as a daemon). | `supertokens start --foreground` | | `--help` | Help for this command. | `supertokens start --help` | ### List List information about all running SuperTokens instances. ```bash supertokens list [options] ``` ### Stop ```bash supertokens stop [options] ``` If you do not provide options, the command stops all instances, or it stops one specific instance of SuperTokens. #### Options | Option | Description | Example | |--------|-------------|---------| | `--id` | Stop an instance of SuperTokens that has a specific `PID`. You can obtain an instance's `PID` via the `supertokens list` command. | `supertokens stop --id=7634` | | `--help` | Help for this command. | `supertokens stop --help` | ### Uninstall Uninstalls SuperTokens ```bash supertokens uninstall [options] ``` #### Manual uninstall ##### 1. Stop or kill all SuperTokens processes ```bash supertokens stop ``` ##### 2. Delete the installation directory You can find out the installation directory by running ```supertokens --help```. ##### 3. Delete the SuperTokens script - Linux: ```/usr/bin/supertokens``` - Mac: ```/usr/local/bin/supertokens``` - Windows: ```C:\Windows\System32\supertokens.bat``` # Platform Configuration - Email delivery Source: https://supertokens.com/docs/platform-configuration/email-delivery ## Overview SuperTokens sends emails in different authentication scenarios. The method applies in the `EmailPassword`, `Passwordless`, and `AccountLinking` recipes. The following page shows you how to configure the email delivery method and adjust the content that gets sent to your users. ## Delivery methods ### Default service If you provide no configuration for email delivery, the backend SDK sends emails by talking to the servers on `https://api.supertokens.com` This applies to both self hosted and managed services. :::important - No info sent to the API that sends out emails on behalf of your app is logged or stored. - The system sends emails using `noreply@supertokens.io` email ID. If you want to use your own domain, please see one of the other methods in this section. - You cannot customize the email template when using this method. If you want to customize the emails, please see one of the other methods in this section. - Emails sent via the service are free and may land up in user's spam due to the high number of apps that use the service. If you want to avoid this, we recommend using one of the other methods mentioned in this section. ::: --- ### SMTP service Using this method, you can provide your own SMTP server's configuration and the system sends the emails using those. Use this method if you want to: - Send emails using your own domain. - Optionally customize the default email template and subject. ```tsx // highlight-start let smtpSettings = { host: "...", authUsername: "...", // this is optional. In case not given, from.email will be used password: "...", port: 465, from: { name: "...", email: "...", }, secure: true } // highlight-end supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ // highlight-start emailDelivery: { service: new SMTPService({smtpSettings}) }, // highlight-end }), // if email verification is enabled.. EmailVerification.init({ mode: "OPTIONAL", // highlight-start emailDelivery: { service: new EmailVerificationSMTPService({smtpSettings}) } // highlight-end }), Session.init() ] }); ``` ```go supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ // highlight-start emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail: async function (input) { // TODO: create and send password reset email // Or use the original implementation which calls the default service, // or a service that you may have specified in the emailDelivery object. return originalImplementation.sendEmail(input); } } } }, // highlight-end }), // if email verification is enabled EmailVerification.init({ mode: "OPTIONAL", // highlight-start emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail: async function (input) { // TODO: create and send email verification email // Or use the original implementation which calls the default service, // or a service that you may have specified in the emailDelivery object. return originalImplementation.sendEmail(input); } } } }, // highlight-end }), Session.init() ] }); ``` ```go ```tsx supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ emailDelivery: { service: new SMTPService({ // @ts-ignore smtpSettings: { /*...*/ }, // highlight-start override: (originalImplementation) => { return { ...originalImplementation, getContent: async function (input) { // password reset content let { passwordResetLink, user } = input; // you can even call the original implementation and modify that let originalContent = await originalImplementation.getContent(input) originalContent.subject = "My custom subject"; return originalContent; } } } // highlight-end }) } }), // if email verification is enabled EmailVerification.init({ mode: "OPTIONAL", emailDelivery: { service: new EmailVerificationSMTPService({ // @ts-ignore smtpSettings: { /*...*/ }, // highlight-start override: (originalImplementation) => { return { ...originalImplementation, getContent: async function (input) { // email verification content let { emailVerifyLink, user } = input; // you can even call the original implementation and modify that let originalContent = await originalImplementation.getContent(input) originalContent.subject = "My custom subject"; return originalContent; } } } // highlight-end }) } }), Session.init() ] }); ``` ```go supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ EmailPassword.init({ // highlight-start emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail: async function (input) { // TODO: run some logic before sending the email await originalImplementation.sendEmail(input); // TODO: run some logic post sending the email } } } }, // highlight-end }), // if email verification is enabled EmailVerification.init({ mode: "OPTIONAL", // highlight-start emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail: async function (input) { // TODO: run some logic before sending the email await originalImplementation.sendEmail(input); // TODO: run some logic post sending the email } } } }, // highlight-end }), Session.init() ] }); ``` ```go # Platform Configuration - SMS Delivery Source: https://supertokens.com/docs/platform-configuration/sms-delivery ## Overview SuperTokens sends SMS in different authentication scenarios. The method applies to the `Passwordless` and `MFA` recipes. The following page shows you how to configure the SMS delivery method and adjust the content that gets sent to your users. ## Delivery methods ### Default method If you provide no configuration for SMS delivery, the backend SDK sends SMSs by talking to SuperTokens servers on `https://api.supertokens.com`. This applies to both self hosted and managed services. :::info Important - This is a free service and is globally rate limited. This is not suitable for production use. If you do not receive an SMS using this method, then the SDK prints the SMS content on the terminal. - We do not log / store any of the info sent to the API that sends out emails on behalf of your app. - You cannot customize the SMS content when using this method. If you want to customize the content, please see one of the other methods in this section. ::: ### Twilio Using this method, you can provide your own Twilio account details to the backend SDK, and the SMS is sent using those. ```tsx supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE", // highlight-start smsDelivery: { service: new TwilioService({ twilioSettings: { accountSid: "...", authToken: "...", opts: { // optionally extra config to pass to Twilio client }, // give either from or messagingServiceSid from: "...", messagingServiceSid: "...", }, }) }, // highlight-end }), Session.init() ] }); ``` ```go supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE", // highlight-start smsDelivery: { service: new SupertokensService("") }, // highlight-end }), Session.init() ] }); ``` ```go supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE", // highlight-start smsDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendSms: async function ({ codeLifetime, // amount of time the code is alive for (in MS) phoneNumber, urlWithLinkCode, // magic link userInputCode, // OTP }) { // TODO: create and send SMS } } } }, // highlight-end }), Session.init() ] }); ``` ```go supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE", smsDelivery: { service: new TwilioService({ // @ts-ignore twilioSettings: { /*...*/ }, // highlight-start override: (originalImplementation) => { return { ...originalImplementation, getContent: async function ({ isFirstFactor, codeLifetime, // amount of time the code is alive for (in MS) phoneNumber, urlWithLinkCode, // magic link userInputCode, // OTP }) { if (isFirstFactor) { // send some custom SMS content return { toPhoneNumber: phoneNumber, body: "SMS BODY" } } else { // for second factor, urlWithLinkCode will always be // undefined since we only support OTP based for second factor return { toPhoneNumber: phoneNumber, body: "SMS BODY" } } // You can even call the original implementation and // modify its content: /*let originalContent = await originalImplementation.getContent(input) originalContent.body = "My custom body"; return originalContent;*/ } } } // highlight-end }) } }), Session.init() ] }); ``` ```go supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE", // highlight-start smsDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendSms: async function (input) { // TODO: before sending SMS await originalImplementation.sendSms(input) // TODO: after sending SMS } } } }, // highlight-end }), Session.init() ] }); ``` ```go # References - Overview Source: https://supertokens.com/docs/references/index Discover the underlying concepts and the APIs exposed by **SuperTokens**. ## References Read through detailed information on entities that you encounter while using **SuperTokens**. Frontend SDK Reference Information on how the frontend SDKs have a structure and how to use them. Backend SDK Reference Information on the structure of the backend SDKs and how to use them. FDI Reference Details about the endpoints exposed by the **Frontend Driver Interface**. This is the API enabled by the backend SDKs. CDI Reference Details about the endpoints exposed by the **Core Driver Interface**. This is the API enabled by the SuperTokens Core Service. ## Advanced customisation See how you can fine-tune your authentication flow by using specific SDK features. Frontend Hooks Frontend Function Overrides Backend Function Overrides Backend API Overrides React Components Override Pre-Built UI Theming # References - How SuperTokens works Source: https://supertokens.com/docs/references/how-supertokens-works ## Overview A SuperTokens deployment consists of three components: - **Frontend SDK**: Responsible for rendering the login UI widgets and managing authentication sessions automatically. - **Backend SDK**: Provides authentication APIs for the frontend and communicates with the SuperTokens core These APIs appear on the same domain as your application's APIs. - **SuperTokens Core**: A HTTP service that contains the core business logic for authentication. It interfaces with the database and gets queried by the backend SDK for operations that require the database. This component can be self hosted by you, inside your own infrastructure. Flowchart of architecture when using SuperTokens managed service Flowchart of architecture when self-hosting SuperTokens Your app's frontend doesn't talk to the SuperTokens core directly. Instead, it talks to the authentication APIs exposed by the backend SDK in your API layer. The SDK then talks to the SuperTokens core. :::caution Important If you need to call the APIs directly keep in mind the following aspects: - You should never call the SuperTokens core API from your frontend. The frontend should only communicate using the routes exposed by the backend SDK. ::: You can follow the next video for a more detailed explanation on how each component works. ## Examples ### Sign in flow :::info Important Whilst this section explains the flow for email / password login, other login methods are similar on a conceptual level - it's with a different set of APIs ::: ## The frontend calls the backend SDK sign in API with the user credentials. ## The backend SDK validates the input and calls the SuperTokens core service with the credentials. ## The core replies with either an `OK` (along with the `userId`) or a `WRONG_CREDENTIALS_ERROR` status string. ## In case of error the backend SDK sends the message to the frontend. The UI then displays an appropriate message to the user. ## In case of success, the backend queries the core with the `userId` to create new session tokens. ## After the core replies with new session tokens, the backend SDK attaches them to the response as cookies or headers and sends them to the frontend. By default, web clients use cookies and all other clients use headers. ## The user logs in. The next high level diagram shows the entire communication flow between the components. Flowchart of sign in flow when using SuperTokens managed service Flowchart of sign in flow when self-hosting SuperTokens ### Session verification flow After sign in, the frontend receives the access and refresh tokens. For web-based clients, the system uses [`HttpOnly` cookies](https://owasp.org/www-community/HttpOnly) by default. The access token is short lived, whilst the refresh token is long lived. #### Refresh flow ## When you make API calls, the access token is automatically attached to the request by the frontend SDK. ## The backend can use the `Verify Session` SDK function to check the validity of the session. ## If the session exists, you get a `session` object in your API. You can use it to remove user information and manipulate the session. ## If the call results in an error, the `Verify Session` function sends a `401` status code to the frontend. ## Upon receiving a `401`, the frontend calls the backend SDK refresh endpoint. ## The backend SDK then calls the SuperTokens core service to generate a new access and refresh tokens. ## The backend SDK sends the new tokens to the frontend. ## The frontend then retries the original request, with the new access token. ## FAQs ### What about OAuth 2.0 flows? If you are familiar with OAuth 2.0 and its flows, you may have noticed that we didn't mention any of them above. That is because, if your application doesn't have multiple websites, all logging in via a common login portal, you don't need OAuth 2.0. In fact, you don't even need OAuth 2.0 if you have multiple sub domains for your application - it can all work using session cookies. However, in case you have applications that are on different domains and need to login via a common site (for example, `foo.com` and `bar.com` both login via `auth.com`), then you require OAuth. For that you can use the `OAuth2` recipe which allows you to implement a common authentication service for [web applications](/docs/authentication/unified-login/introduction) or [microservices](/docs/authentication/m2m/introduction). ### What is a recipe? Each login method is a separate recipe. If you want email password login, you should see the [email password recipe docs](/docs/authentication/email-password/introduction), for passwordless, you should see the [passwordless recipe docs](/docs/authentication/passwordless/introduction). You can use multiple recipes at once too. Most of these guides use the [Session recipe](/docs/post-authentication/session-management/introduction) along with one of the login recipes. SuperTokens divides its structure into recipes because when using a recipe, all the configurations and types are specific to that recipe's feature set. This, in turn, makes it easier for you to customize the auth logic. # References - Plugins - CAPTCHA Plugin Reference Source: https://supertokens.com/docs/references/plugins/captcha-reference ## [`@supertokens-plugins/captcha-react`](https://github.com/supertokens/supertokens-plugins/tree/main/packages/captcha-react) ### [init](https://github.com/supertokens/supertokens-plugins/tree/main/packages/captcha-react/src/plugin.ts#L16) {#captcha-react-init} Promise)", required: false, } ]} > ### [SuperTokensPluginCaptchaConfig](https://github.com/supertokens/supertokens-plugins/tree/main/packages/captcha-react/src/types.ts#L77) {#captcha-react-supertokensplugincaptchaconfig} void", required: false, } ]} > ### [useCaptcha](https://github.com/supertokens/supertokens-plugins/tree/main/packages/captcha-react/src/hooks/useCaptcha.ts#L8) {#captcha-react-usecaptcha}
## [`@supertokens-plugins/captcha-nodejs`](https://github.com/supertokens/supertokens-plugins/tree/main/packages/captcha-nodejs) {#captcha-nodejs-init} ### [init](https://github.com/supertokens/supertokens-plugins/tree/main/packages/captcha-nodejs/src/plugin.ts#L6) ### [SuperTokensPluginCaptchaConfig](https://github.com/supertokens/supertokens-plugins/tree/main/packages/captcha-nodejs/src/types.ts#L35) {#captcha-nodejs-supertokensplugincaptchaconfig} # References - Frontend SDKs - Frontend SDKs Source: https://supertokens.com/docs/references/frontend-sdks/reference ## Overview SuperTokens exposes SDKs for frontend frameworks. Use this page to find references to each of them and about specific functionalities. ## Customization Function overrides Hooks Component override Prebuilt UI styling Prebuilt UI theming Translations ## SDK references ReactJS Vanilla JS React Native iOS Android Flutter ## SDK configuration The `appInfo` object is the paramter used to configure the SDKs during initialization. ```ts let appInfo: { appName: string, websiteDomain: string, apiDomain: string, websiteBasePath?: string, apiBasePath?: string, apiGatewayPath?: string } ``` ## `appName` This is the name of your application. Use it when sending password reset or email verification emails (in the default email design). An example of this is `appName: "GitHub"`.
## `websiteDomain` This is the domain part of your website. This is where the login UI appears. For example: - For local development, you are likely using `localhost` with some port (ex `8080`). Then the value of this should be `"http://localhost:8080"`. - If your website is `https://www.example.com`, then the value of this should be `"https://www.example.com"`. - If your website is `https://example.com`, then the value of this should be `"https://example.com"`. - If you have multiple sub domains, and your users login via `https://auth.example.com`, then the value of this should be `"https://auth.example.com"`. By default, the login UI appears on `{websiteDomain}/auth/*`. This configuration can change by using the `websiteBasePath` configuration. On the frontend, the domain serves routing purposes, and on the backend, it generates correct email verification and password reset links.
## `apiDomain` This is the domain part of your API endpoint that the frontend talks to. For example: - For local development, you are likely using `localhost` with some port (ex `9000`). Then the value of this should be `"http://localhost:9000"`. - If your frontend queries `https://api.example.com/*`, then the value of this should be `"https://api.example.com"` - If your API endpoint reaches `/api/*`, then the value of this is the same as the `websiteDomain` - since `/api/*` is equal to querying `{websiteDomain}/api/*`. By default, the login widgets query `{apiDomain}/auth/*`. This configuration can change by using the `apiBasePath` configuration.
## `websiteBasePath` By default, the login UI appears on `{websiteDomain}/auth`. Other authentication-related user interfaces appear on `{websiteDomain}/auth/*`. If you want to change the `/auth` to something else, then you must set this value. For example: - If you want the login UI to show on `{websiteDomain}/user/*`, then the value of this should be `"/user"`. - If you are using a dedicated sub domain for auth, like `https://auth.example.com`, then you probably want the login UI to show up on `https://auth.example.com`. In this case, set this value to `"/"`. :::important Remember to set the same value for this parameter on the backend and the frontend. :::
## `apiBasePath` By default, the frontend SDK queries `{apiDomain}/auth/*`. If you want to change the `/auth` to something else, then you must set this value. For example: - If you have versioning in your API path and want to query `{apiDomain}/v0/auth/*`, then the value of this should be `"/v0/auth"`. - If you want to scope the APIs not via `/auth` but via some other string like `/supertokens`, then you can set the value of this to `"/supertokens"`. This means, the APIs appear on `{apiDomain}/supertokens/*`. - If you do not want to scope the APIs at all, then you can set the values of this to be `"/"`. This means the APIs are available on `{apiDomain}/*` :::important Remember to set the same value for this parameter on the backend and the frontend. ::: :::caution Note that setting a custom `apiBasePath` updates the refresh API path, this can cause an issue where previously issued refresh tokens no longer get sent to the new API endpoint and the user logs out. For example, the default `apiBasePath` value is `/auth`, if it changes to `/supertokens`, then your refresh endpoint updates from `/auth/session/refresh` to `/supertokens/session/refresh`. Previously issued refresh tokens do not get sent to the new API endpoint and the user logs out. :::
## `apiGatewayPath` :::note Most relevant if you are using an API gateway or reverse proxy ::: If you are using an API gateway (like the one provided by AWS) or a reverse proxy (like Nginx), it may add a path to your API endpoints to scope them for different development environments. For example, your APIs for development appear via `{apiDomain}/dev/*`, and for production, they may appear via `{apiDomain}/prod/*`. Whilst the frontend would need to use the `/dev/` and `/prod/`, your backend code does not see that sub path (the gateway removes `/dev/` and `/prod/`). For these situations, you should set the `apiGatewayPath` to `/dev` or `/prod`. For example: - If your API gateway is using `/development` for scoping, and you want to expose the SuperTokens APIs on `/supertokens/*`, then set `apiGatewayPath: "/development"` & `apiBasePath: "/supertokens"`. This means that the frontend SDK queries `{apiDomain}/development/supertokens/*` to reach the endpoints exposed by SuperTokens. - If you set this and not `apiBasePath`, then the frontend SDK queries `{apiDomain}{apiGatewayPath}/auth/*` to reach the endpoints exposed by SuperTokens. The reason for this distinction between `apiGatewayPath` and `apiBasePath` is that when routing, the backend SDK does not see the `apiGatewayPath` path from the request as the gateway removes them. Taking the above example, whilst the frontend queries `{apiDomain}/development/supertokens/*`, the backend SDK sees `{apiDomain}/supertokens/*`.
# References - Frontend SDKs - Function overrides Source: https://supertokens.com/docs/references/frontend-sdks/function-overrides ## Overview **Function overrides** let you customize the behavior of the functions used internally, by the SDKs. You can change how actions like signing in, signing up, creating, or revoking sessions or signing out work. This flexibility lets you integrate your own logic into the authentication and session management processes. For example, if a recipe checks for an active session using the session recipe’s `doesSessionExist` function, you can override that function to work with your custom session management. Similarly, if you already have a sign-in/sign-up flow and want to integrate with SuperTokens, overriding allows you to handle the migration process. You can even implement your own `userId` format by mapping your `userIds` to those generated by SuperTokens. ## Before you start This page is relevant if you are using the actual frontend SDK. If you are calling the backend SDK endpoints directly this code does not run in your use case. ## Example The code snippet shows the general flow of overriding a function. You inject your own custom logic while also calling the original implementation of the function. :::info The next examples include a couple of override samples. See all the [functions that can be overridden here](https://supertokens.com/docs/auth-react/modules/recipe_thirdpartyemailpassword.html#RecipeInterface) ::: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ Session.init({ // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, // we will only be overriding the function for checking // if a session exists doesSessionExist: async function (input) { // TODO: some custom logic // or call the default behaviour as show below return originalImplementation.doesSessionExist(input); } } } } // highlight-end }), EmailPassword.init({ // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, // we will only be overriding what happens when a user // clicks the sign up button. signUp: async function (input) { // TODO: some custom logic // or call the default behaviour as show below return originalImplementation.signUp(input); }, // ... // TODO: override more functions } } } // highlight-end }), ThirdParty.init({ // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, // we will only be overriding what happens when a user // clicks the sign in or sign up button. signInAndUp: async function (input) { // TODO: some custom logic // or call the default behaviour as show below return originalImplementation.signInAndUp(input); }, // ... // TODO: override more functions } } } // highlight-end }) ] }); ``` :::info See all the [functions that can be overridden here](https://supertokens.com/docs/auth-react/modules/recipe_thirdpartyemailpassword.html#RecipeInterface) ::: ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, recipeList: [ supertokensUISession.init({ // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, // we will only be overriding the function for checking // if a session exists doesSessionExist: async function (input) { // TODO: some custom logic // or call the default behaviour as show below return originalImplementation.doesSessionExist(input); } } } } // highlight-end }), supertokensUIEmailPassword.init({ // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, // we will only be overriding what happens when a user // clicks the sign up button. signUp: async function (input) { // TODO: some custom logic // or call the default behaviour as show below return originalImplementation.signUp(input); }, // ... // TODO: override more functions } } } // highlight-end }), supertokensUIThirdParty.init({ // highlight-start override: { functions: (originalImplementation) => { return { ...originalImplementation, // we will only be overriding what happens when a user // clicks the sign in or sign up button. signInAndUp: async function (input) { // TODO: some custom logic // or call the default behaviour as show below return originalImplementation.signInAndUp(input); }, // ... // TODO: override more functions } } } // highlight-end }) ] }); ``` # References - Frontend SDKs - Hooks Source: https://supertokens.com/docs/references/frontend-sdks/hooks ## Overview Hooks are a way to trigger custom logic when certain actions happen in the authentication process. --- ## Handle event hook Each frontend recipe emits events when certain actions happen. You can use this hook to trigger side effects when something happens in the authentication process. This can address things like logging or analytics. ```tsx EmailPassword.init({ onHandleEvent: (context) => { if (context.action === "PASSWORD_RESET_SUCCESSFUL") { } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { } else if (context.action === "SUCCESS") { if (context.createdNewSession) { let user = context.user; if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // sign up success } else { // sign in success } } else { // during step up or second factor auth with email password } } } }) ThirdParty.init({ onHandleEvent: (context) => { if (context.action === "SUCCESS") { if (context.createdNewSession) { let user = context.user; if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // sign up success } else { // sign in success } } else { // during linking a social account to an existing account } } } }) ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIEmailPassword.init({ onHandleEvent: (context) => { if (context.action === "PASSWORD_RESET_SUCCESSFUL") { } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { } else if (context.action === "SUCCESS") { if (context.createdNewSession) { let user = context.user; if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // sign up success } else { // sign in success } } else { // during step up or second factor auth with email password } } } }) supertokensUIThirdParty.init({ onHandleEvent: (context) => { if (context.action === "SUCCESS") { if (context.createdNewSession) { let user = context.user; if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // sign up success } else { // sign in success } } else { // during linking a social account to an existing account } } } }) ``` :::caution Not applicable since you need to build custom UI anyway. When you call the functions from the SDK, or call the API directly, you can run custom logic in your own code. ::: --- ## Pre-API hook This function calls the backend before any API call. You can use this to change the request properties. ```tsx ThirdParty.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "GET_AUTHORISATION_URL") { } else if (action === "THIRD_PARTY_SIGN_IN_UP") { // Note: this could either be sign in or sign up. // we don't know that at the time of the API call // since all we have is the authorisation code from // the social provider } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url }; } }) EmailPassword.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_EXISTS") { } else if (action === "EMAIL_PASSWORD_SIGN_IN") { } else if (action === "EMAIL_PASSWORD_SIGN_UP") { } else if (action === "SEND_RESET_PASSWORD_EMAIL") { } else if (action === "SUBMIT_NEW_PASSWORD") { } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url }; } }) ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIThirdParty.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "GET_AUTHORISATION_URL") { } else if (action === "THIRD_PARTY_SIGN_IN_UP") { // Note: this could either be sign in or sign up. // we don't know that at the time of the API call // since all we have is the authorisation code from // the social provider } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url }; } }) supertokensUIEmailPassword.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_EXISTS") { } else if (action === "EMAIL_PASSWORD_SIGN_IN") { } else if (action === "EMAIL_PASSWORD_SIGN_UP") { } else if (action === "SEND_RESET_PASSWORD_EMAIL") { } else if (action === "SUBMIT_NEW_PASSWORD") { } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url }; } }) ``` ```tsx EmailPassword.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_EXISTS") { } else if (action === "EMAIL_PASSWORD_SIGN_IN") { } else if (action === "EMAIL_PASSWORD_SIGN_UP") { } else if (action === "SEND_RESET_PASSWORD_EMAIL") { } else if (action === "SUBMIT_NEW_PASSWORD") { } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url }; }, }) ThirdParty.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "GET_AUTHORISATION_URL") { } else if (action === "THIRD_PARTY_SIGN_IN_UP") { // Note: this could either be sign in or sign up. // we don't know that at the time of the API call // since all we have is the authorisation code from // the social provider } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url }; }, }) ``` Alternatively you could also declare the pre-API hook when calling the function: ```tsx EmailPassword.doesEmailExist({ email: "...", options: { preAPIHook: async (input) => { let url = input.url let requestInit = input.requestInit // TODO: add your code here return {url, requestInit}; }, } }); ``` ```tsx supertokensEmailPassword.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_EXISTS") { } else if (action === "EMAIL_PASSWORD_SIGN_IN") { } else if (action === "EMAIL_PASSWORD_SIGN_UP") { } else if (action === "SEND_RESET_PASSWORD_EMAIL") { } else if (action === "SUBMIT_NEW_PASSWORD") { } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url }; }, }) supertokensThirdParty.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "GET_AUTHORISATION_URL") { } else if (action === "THIRD_PARTY_SIGN_IN_UP") { // Note: this could either be sign in or sign up. // we don't know that at the time of the API call // since all we have is the authorisation code from // the social provider } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url }; }, }) ``` Alternatively you could also declare the pre-API hook when calling the function: ```tsx supertokensEmailPassword.doesEmailExist({ email: "...", options: { preAPIHook: async (input) => { let url = input.url let requestInit = input.requestInit return {url, requestInit}; }, } }); ``` ```tsx SuperTokens.init({ apiDomain: "...", preAPIHook: async (context) => { let requestInit = context.requestInit; if (context.action === "REFRESH_SESSION") { requestInit.headers = { ...requestInit.headers, customHeader: "custom-header", }; } else if (context.action === "SIGN_OUT") { requestInit.headers = { ...requestInit.headers, customHeader: "custom-header", }; } return { ...context, requestInit, }; }, }); ``` ```kotlin void main() { SuperTokens.init( apiDomain: "...", preAPIHook: (action, req) { if (action == APIAction.SIGN_OUT) { req.headers["custom-header"] = "custom-value"; } else if (action == APIAction.REFRESH_TOKEN) { req.headers["custom-header"] = "custom-value"; } return req; }, ); } ``` --- ## Redirection callback hook Use this function to change where the system redirects the user after certain actions. For example, you can use this to redirect a user to a specific URL post sign in or sign up. If you're embedding the UI components in a popup and wish to disable redirection entirely, return `null`. ```tsx SuperTokens.init({ appInfo: { appName: "SuperTokens", apiDomain: "http://localhost:3000", websiteDomain: "http://localhost:3000" }, getRedirectionURL: async (context) => { if (context.action === "SUCCESS" && context.newSessionCreated) { // called on a successful sign in / up. Where should the user go next? let redirectToPath = context.redirectToPath; if (redirectToPath !== undefined) { // we are navigating back to where the user was before they authenticated return redirectToPath; } if (context.createdNewUser) { // user signed up return "/onboarding" } else { // user signed in return "/dashboard" } } else if (context.action === "TO_AUTH") { // called when the user is not authenticated and needs to be redirected to the auth page. return "/auth"; } // return undefined to let the default behaviour play out return undefined; }, recipeList: [ EmailPassword.init({ getRedirectionURL: async (context) => { if (context.action === "RESET_PASSWORD") { // called when the user clicked on the forgot password button } // return undefined to let the default behaviour play out return undefined; } })] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "SuperTokens", apiDomain: "http://localhost:3000", websiteDomain: "http://localhost:3000" }, getRedirectionURL: async (context) => { if (context.action === "SUCCESS" && context.newSessionCreated) { // called on a successful sign in / up. Where should the user go next? let redirectToPath = context.redirectToPath; if (redirectToPath !== undefined) { // we are navigating back to where the user was before they authenticated return redirectToPath; } if (context.createdNewUser) { // user signed up return "/onboarding" } else { // user signed in return "/dashboard" } } else if (context.action === "TO_AUTH") { // called when the user is not authenticated and needs to be redirected to the auth page. return "/auth"; } // return undefined to let the default behaviour play out return undefined; }, recipeList: [ supertokensUIEmailPassword.init({ getRedirectionURL: async (context) => { if (context.action === "RESET_PASSWORD") { // called when the user clicked on the forgot password button } // return undefined to let the default behaviour play out return undefined; } })] }); ``` :::caution Not applicable since you need to build custom UI anyway. When you call the functions from the SDK, or call the API directly, you can run custom logic in your own code. ::: # References - Frontend SDKs - User Interface - UI showcase Source: https://supertokens.com/docs/references/frontend-sdks/prebuilt-ui/ui-showcase ## Overview The following page shows the interfaces exposed the pre-built UI for each authentication flow and recipe. --- ## Email password login You can see the default UI when you visit the `websiteBasePath` (`/auth` by default) on your website. The form includes a sign in and a sign up form, and by default, the sign in form appears. ## Default sign in form Sign in form UI for email password login ## Default sign up form Sign up form UI for email password login ## Field validation error UI The sign up form checks if the email is unique and if not, shows an error to the user saying that the email already exists. Likewise, if any of the form field's validation fails, the user sees an error like below. Sign up form field error UI ## Incorrect email / password UI In the sign in form, if the user enters an email that doesn't exist, or is an incorrect email & password combination, they see the following error. Sign in form wrong credentials UI ## General error UI If there are network related errors, or the backend sends a status code >= 300, then the following UI appears. Sign in form general error --- ## Email verification ## Email sent screen UI Once the user visits the email verification screen, this is what they see. Email verification email sent On screen load, SuperTokens automatically sends an email to the user. This way, the user doesn't have to do any interaction before they get an email. Customizing this behavior is possible via the overrides feature. The email verification link is of the format: `//verify-email?token=...&tenantId=....` ## Post link clicked screen ### If a session exists In this case, the API automatically consumes the email verification link and shows the following UI. Email verification success Clicking on the continue button takes the user to the post sign in / up page (`/` by default). ### If a session does not exist In this case, the user must do an interaction before calling the API to consume the token. This prevents email clients from automatically verifying the email since many of them may crawl the link in the email. The user first sees this screen Email verification no session prompt And then after they click on continue, they see the same screen as when a session did exist. ### Expired link UI If the user clicks on an email link that has expired, they see the following UI. Email verification no session prompt After clicking continue, they return to the email sent screen (if a session exists), or to the sign in page (if a session doesn't exist). ## General error UI If there are network related errors, or the backend sends a status code >= 300, then the following UI appears. Email verification generic error The below appears if something went wrong when the user clicked on the email verification link. To try again, they have to reload the page. Email verification link clicked generic error ## Default email UI The default email sent for email verification appears below. The backend SDK sends it, which calls `https://api.supertokens.com` (the API infrastructure). Follow the link at the end of this page to see how you can change the email content or delivery method. Email UI for email verification email --- ## Password reset ## Enter email form This appears when the user clicks on the "Forgot password" button in the sign in form. You can view it if you visit `/${websiteBasePath}/reset-password` path of your website (default is `/auth/reset-password`). Enter email in reset password form Once the user enters their email and clicks on the "Email" button, SuperTokens sends them an email only if that email belongs to an account. Regardless, the user always sees a success state: Email sent in reset password form ## Enter new password form This form appears when the user clicks on the password reset link sent to their email. To view this form, you can navigate to `/${websiteBasePath}/reset-password?token=test` path of your website (default is `/auth/reset-password?token=test`). Notice that the URL path is the same as that of the enter email form. However, there is an extra query parameter `token` which tells SuperTokens to show the enter new password form. If you try and submit a new password with the `test` token value, it fails since it's not a valid password reset token. Enter new password form If the reset token has expired or is invalid, the user sees the following message. Enter new password for invalid token Once the user has successfully changed their password, they see the following success screen Password change successful :::info Multi tenancy For multi tenant use case, the password reset token also includes a `tenantId` query parameter which identifies the tenant for which the system created the password reset token. ::: ## General error UI If there are network related errors, or the backend sends a status code >= 300, then the following UI appears. Enter email general error Enter new password general error ## Password reset email UI The default email sent for password reset appears below. The backend SDK sends it, which calls `https://api.supertokens.com` (the API infrastructure). See the links at the end of this page to change the email content or delivery method. Email UI for password reset email --- ## Passwordless login ## Email or phone input UI If the `contactMethod` is `EMAIL_OR_PHONE`, the user sees the following UI when they visit the login page. Email and phone passwordless login If the user decides to use their phone number and enters a valid phone number with their country code extension, they proceed to the next step. Otherwise, they see an error message asking them to also enter their country code. The UI also changes to show a dropdown containing a list of all countries (equal to the "Only phone input UI" shown below). ## Only email input UI If the `contactMethod` is `EMAIL`, the user sees the following UI when they visit the login page. Email only passwordless login ## Only phone input UI If the `contactMethod` is `PHONE`, the user sees the following UI when they visit the login page. Phone only passwordless login ## Invalid email or phone input UI If the user enters an invalid phone or email, they see the following message Invalid phone or email ## Magic link sent screen If the value of `flowType` on the backend is `MAGIC_LINK`, then after the user has submitted their phone or email, they see the following UI. Magic link sent UI As you can see, a timer makes the user wait for a certain time (15 seconds by default) before they can resend the SMS / email. A button below the input allows them to change the email / SMS (the text on the button changes based on if the user entered an email or phone number). ## Magic link clicked screens The magic link is of the format: `//verify?tenantId=...&preAuthSessionId=#`. ### On same device When the user clicks the magic link and opens it on the same device on which they initiated the flow, they see the following UI before redirecting to the sign in success page (`/` by default). On this screen, SuperTokens automatically extracts the two tokens from the magic link URL. It tries to consume them on the backend to log the user in. Magic link consumed ### On different device If the user opens the magic link on a different device, they must take an action before consuming tokens from the link. This prevents email clients from automatically consuming the tokens if they crawl links in the email. Magic link consumed on different device ### Invalid / expired magic link UI If the user clicks on an invalid magic link or if the token in the magic link has expired, they see the login screen with the following message Invalid magic link ## Enter OTP screen If login via OTP is active, then the user sees this screen immediately after their enter their phone number / email. Enter OTP screen As you can see, a timer makes the user wait for a certain time (15 seconds by default) before they can resend the SMS / email. A button below the input allows them to change the email / SMS (the text on the button changes based on if the user entered an email or phone number). ### Invalid OTP If the user enters an incorrect OTP, this is what they see. Invalid OTP Entering an incorrect OTP too many times results in the user navigating back to the login screen with the following message. Entered invalid OTP too many times ### Logging in via OTP and Magic link simultaneously An edge case occurs wherein the end user gets both an OTP and a magic link. Whilst viewing the enter OTP screen, they also click on the magic link. The magic link click opens a new tab and consumes the link to log the user in. The enter OTP screen continues to show the enter OTP UI until the user refreshes the page. After the refresh, it redirects to the post login screen. ## Default email and SMS template You can find the email and SMS templates along with their UI [in one of the GitHub repositories](https://github.com/supertokens/email-sms-templates). You can change the content of the email & SMSs and / or how they send. For more information on this, please see the links at the end of this page. ## General errors If there are network related errors, or the backend sends a status code >= 300, then the following UI shows. This UI also appears if there is a similar error in the callback page. Login screen general error Enter OTP screen general error Magic link sent screen general error The error below appears if something went wrong after the user clicks on the magic link. Reloading the page should result in a reattempt Email verification link clicked generic error --- ## Social login You can see the default UI when you visit the `websiteBasePath` (`/auth` by default) on your website. ## Default social login button UI Sign in form UI for social login ## Callback page UI When the user navigates back to your application from the third party provider, they see the following UI. On this screen, SuperTokens takes the authorisation code from the URL (sent by the provider) and sends it to the backend. The backend exchanges that for the user's access token and information. Callback screen from third party provider. ## Sign in / up unsuccessful UI SuperTokens requires that the third party provider gives an email for the user. If that's not the case for a certain provider, the user sees the following UI. No email UI for provider ## General error UI If there are network related errors, or the backend sends a status code >= 300, then the following UI shows. This UI also appears if there is a similar error in the callback page. Sign in form general error # References - Frontend SDKs - User Interface - ## Overview Source: https://supertokens.com/docs/references/frontend-sdks/prebuilt-ui/changing-colours You can update the default theme with your colors to make it fit with your website. Define a few CSS variables in the `style` property to the `EmailPassword.init` call. Specify the colors as RGB (see the following example), because the `rgb` and `rgba` functions apply them. For example, if your website uses a dark theme, here is how you can customize it: ## Before you start :::caution no-title This example is relevant only if you use the prebuilt UI components. ::: ## Example ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, // highlight-start style: ` [data-supertokens~=container] { --palette-background: 51, 51, 51; --palette-inputBackground: 41, 41, 41; --palette-inputBorder: 41, 41, 41; --palette-textTitle: 255, 255, 255; --palette-textLabel: 255, 255, 255; --palette-textPrimary: 255, 255, 255; --palette-error: 173, 46, 46; --palette-textInput: 169, 169, 169; --palette-textLink: 114,114,114; --palette-textGray: 158, 158, 158; } `, // highlight-end recipeList: [ /* ... */] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, // highlight-start style: ` [data-supertokens~=container] { --palette-background: 51, 51, 51; --palette-inputBackground: 41, 41, 41; --palette-inputBorder: 41, 41, 41; --palette-textTitle: 255, 255, 255; --palette-textLabel: 255, 255, 255; --palette-textPrimary: 255, 255, 255; --palette-error: 173, 46, 46; --palette-textInput: 169, 169, 169; --palette-textLink: 114,114,114; --palette-textGray: 158, 158, 158; } `, // highlight-end recipeList: [ /* ... */] }); ``` Prebuilt form UI with custom color palette :::important Changes to the palette apply to all the UI components provided. If you want to change a specific component, please see [this section](changing-style). ::: ### Palette values | Variable Name | Description | Default Value | |--------------|-------------|---------------| | `background` | Background color of all forms | `255, 255, 255` (white) | | `inputBackground` | Background color of input fields | `250, 250, 250` (light grey) | | `inputBorder` | Border color of input fields | `224, 224, 224` (light grey) | | `primary` | Primary color for focused inputs, success states and button backgrounds | `28, 34, 42` | | `primaryBorder` | Border color for primary buttons | `45, 54, 68` | | `success` | Color used for success events | `65, 167, 0` (green) | | `successBackground` | Background color for success notifications | `217, 255, 191` (green) | | `error` | Color for error highlights and messages | `255, 23, 23` (red) | | `errorBackground` | Background color for error notifications | `255, 241, 235` (red) | | `textTitle` | Color of form titles | `0, 0, 0` (black) | | `textLabel` | Color of form field labels | `0, 0, 0` (black) | | `textInput` | Color of text in form fields | `0, 0, 0` (black) | | `textPrimary` | Color of subtitles and footer text | `128, 128, 128` (grey) | | `textLink` | Color of links | `0, 122, 255` (blue) | | `buttonText` | Color of text in main buttons | `255, 255, 255` (white) | | `superTokensBrandingBackground` | Color of SuperTokens branding element | `242, 245, 246` (Alice blue) | | `superTokensBrandingText` | Color of "Powered by SuperTokens" text | `173, 189, 196` (heather grey) | # References - Frontend SDKs - User Interface - ## Overview Source: https://supertokens.com/docs/references/frontend-sdks/prebuilt-ui/changing-style Updating the CSS allows you to change the UI of the components to meet your needs. This section guides you through an example of updating the look of buttons. Note that the process can update any HTML tag from within SuperTokens components. ## Before you start :::caution no-title This example is relevant only if you use the prebuilt UI components. ::: --- ## Global style changes First, open the website at `/auth`. The Sign-in widget should show up. Use the browser console to find out the class name that you'd like to overwrite. Inspecting submit button in prebuilt form Highlighting attribute for customization Each stylable component contains `data-supertokens` attributes (in this example `data-supertokens="button"`). Let's customize elements with the `button` attribute. The syntax for styling is plain CSS. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, // highlight-start style: ` [data-supertokens~=button] { background-color: #252571; border: 0px; width: 30%; margin: 0 auto; } `, // highlight-end recipeList: [ /* ... */] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, // highlight-start style: ` [data-supertokens~=button] { background-color: #252571; border: 0px; width: 30%; margin: 0 auto; } `, // highlight-end recipeList: [ /* ... */] }); ``` The above results in: Prebuilt form with custom submit button ### Changing fonts By default, SuperTokens uses the `Arial` font. The best way to override this is to add a `font-family` styling to the `container` component in the recipe configuration. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, style: ` [data-supertokens~=container] { font-family: cursive; } `, recipeList: [ /* ... */] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, style: ` [data-supertokens~=container] { font-family: cursive; } `, recipeList: [ /* ... */] }); ``` ### Using media queries You may want to have different CSS for different `viewports`. This can happen via media queries like this: ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, style: ` [data-supertokens~=button] { background-color: #252571; border: 0px; width: 30%; margin: 0 auto; } @media (max-width: 440px) { [data-supertokens~=button] { width: 90%; } } `, recipeList: [ /* ... */], }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, style: ` [data-supertokens~=button] { background-color: #252571; border: 0px; width: 30%; margin: 0 auto; } @media (max-width: 440px) { [data-supertokens~=button] { width: 90%; } } `, recipeList: [ /* ... */], }); ``` --- ## Customize the sign up and sign in forms These are the screens shown when the user tries to log in or sign up for the application. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, style: `[data-supertokens~=authPage] { ... }`, recipeList: [ /* ... */] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, style: `[data-supertokens~=authPage] { ... }`, recipeList: [ /* ... */] }); ``` --- ## Customize the password reset forms ### Send password reset email form This form appears when the user clicks on "forgot password" in the sign in form. ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ // highlight-start resetPasswordUsingTokenFeature: { enterEmailForm: { style: ` ... ` } } // highlight-end }), Session.init() ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ // highlight-start resetPasswordUsingTokenFeature: { enterEmailForm: { style: ` ... ` } } // highlight-end }), supertokensUISession.init() ] }); ``` ### Submit new password form This screen appears when the user clicks the password reset link on their email - to enter their new password ```tsx SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ // highlight-start resetPasswordUsingTokenFeature: { submitNewPasswordForm: { style: ` ... ` } } // highlight-end }), Session.init() ] }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ // highlight-start resetPasswordUsingTokenFeature: { submitNewPasswordForm: { style: ` ... ` } } // highlight-end }), supertokensUISession.init() ] }); ``` # References - Frontend SDKs - User Interface - Override react components Source: https://supertokens.com/docs/references/frontend-sdks/prebuilt-ui/override-react-components ## Overview SuperTokens allows you to customize the React components by overriding them. In this context, an override is a new component that renders additional data or adds new functionality. Each override should also render the original component to ensure the integrity of the authentication UI. ## Before you start :::caution no-title This feature is only applicable to React apps that use the prebuilt UI. ::: ## Steps ### 1. Figure out which component to override Discover the name of the component that you need to override by using the React Developer Tools extension. Look for the names defined in component override configuration and/or components ending in `_Override` in the component tree. Checking which component from the prebuilt UI is overridden using React Developer Tools extension ### 2. Add your override Inside the `SuperTokensWrapper`, update the recipe specific override context with your next component. Make sure that it your override renders the SuperTokens components inside it. ```tsx // @ts-ignore function App() { return ( { return (
); }, }}> { // your customisations here for the email password sign up form... return ; }, }}> { // optionally override the third party providers list.. return ; } }}> {/* Rest of the JSX */}
); } export default App; ```
```tsx // @ts-ignore function App() { if(canHandleRoute([EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI])){ return ( { return (
); }, }}> { // your customisations here for the email password sign up form... return ; }, }}> { // optionally override the third party providers list.. return ; } }}> {getRoutingComponent([EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI])}
) } return ( {/* Rest of the JSX */} ); } export default App; ```
Prebuilt sign in UI with custom image :::important Please make sure that you specify the configuration in a `.tsx` or ` .jsx` file type. ::: # References - Frontend SDKs - User Interface - ## Before you start Source: https://supertokens.com/docs/references/frontend-sdks/prebuilt-ui/embed-sign-in-up-form :::caution no-title This example is relevant only if you use the React SDK with prebuilt UI components. ::: --- ## Render the form in a page The following example shows the scenario where you have a dedicated route, such as `/auth`, for rendering the Auth Widget. Upon a successful login, the user automatically redirects to the return value of `getRedirectionURL` (defaulting to `/`). ```tsx // highlight-start // highlight-end // @ts-ignore // @ts-ignore SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, // highlight-start getRedirectionURL: async (context) => { if (context.action === "TO_AUTH") { return "/auth"; // return the path where you are rendering the Auth UI } else if (context.action === "SUCCESS" && context.newSessionCreated) { return "/dashboard"; // defaults to "/" }; }, disableAuthRoute: true, // highlight-end recipeList: [ /* ... */], }); function MyAuthPage() { const navigate = useNavigate(); return (
// highlight-next-line
); } ```
```tsx // highlight-start // highlight-end // @ts-ignore // @ts-ignore SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, // highlight-start getRedirectionURL: async (context) => { if (context.action === "TO_AUTH") { return "/auth"; // return the path where you are rendering the Auth UI } else if (context.action === "SUCCESS" && context.newSessionCreated) { return "/dashboard"; // defaults to "/" }; }, disableAuthRoute: true, // highlight-end recipeList: [ /* ... */], }); function MyAuthPage() { const history = useHistory(); return (
// highlight-next-line
); } ```
```tsx // highlight-start // highlight-end // @ts-ignore // @ts-ignore SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, // highlight-start getRedirectionURL: async (context) => { if (context.action === "TO_AUTH") { return "/auth"; // return the path where you are rendering the Auth UI } else if (context.action === "SUCCESS" && context.newSessionCreated) { return "/dashboard"; // defaults to "/" }; }, disableAuthRoute: true, // highlight-end recipeList: [ /* ... */], }); function MyAuthPage() { return (
// highlight-next-line
) } ```
In the above code snippet: 1. Disabled the default Auth UI by setting `disableAuthRoute` to `true`. 2. Override the `getRedirectionURL` function inside the SuperTokens configuration to redirect to `/auth` when login becomes necessary and to redirect to `/dashboard` upon successful login. Feel free to customize the redirection URLs as needed. :::note When the user visits the `/auth` page, they see the SignIn UI by default. To render the SignUp UI, append `show=signup` as a query parameter to the URL, like`/auth?show=signup`. ::: --- ## Render the form in a page with no redirection The following example shows the scenario where you have a dedicated route, such as `/auth`, for rendering the Auth Widget. However, upon a successful login, the user sees a logged in UI instead of getting redirected. ```tsx // highlight-start // highlight-start // highlight-end // @ts-ignore // @ts-ignore SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, disableAuthRoute: true, recipeList: [ /* ... */], // highlight-start getRedirectionURL: async (context) => { if (context.action === "SUCCESS") { return null; // this will not navigate the user away after successful login } }, // highlight-end }); // highlight-start function LandingPage() { let sessionContext = Session.useSessionContext(); const navigate = useNavigate(); if (sessionContext.loading) { return null; } if (sessionContext.doesSessionExist) { // We wrap this with so that // all claims are validated before showing the logged in UI. // For example, if email verification is switched on, and // the user's email is not verified, then // will redirect to the email verification page. return (
You are logged in!