Skip to content
Esc
navigateopen⌘Jpreview
Dashboard
On this page

Link social accounts

Link social accounts using the account linking feature.

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

Enable paid features

This feature is only available to paid users. Follow the instructions below to enable it.

Managed Service

  1. Sign in to the SuperTokens dashboard.
  2. Select the managed service option from the service type select component.
  3. Select your core instance from the next elemenet or create a new one.
  4. Open Features sub-page and enable the required ones.

Self Hosted

  1. Sign in to the SuperTokens dashboard.
  2. Select the self-hosted option from the service type select component.
  3. Select your license key from the next elemenet or create a new one. Then enable the required features.
  4. If the key is not yet configured, add it to your Core service. If your Core already uses this key, no configuration changes are required.

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

import supertokens, { User, RecipeUserId } from "supertokens-node";
import AccountLinking from "supertokens-node/recipe/accountlinking";
import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types";
import { SessionContainerInterface } from "supertokens-node/recipe/session/types";

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,
      ) => {
        if (user === undefined) {
          return {
            shouldAutomaticallyLink: true,
            shouldRequireVerification: true,
          };
        }
        if (session !== undefined && session.getUserId() === user.id && session.getTenantId() === tenantId) {
          return {
            shouldAutomaticallyLink: true,
            shouldRequireVerification: true,
          };
        }
        return {
          shouldAutomaticallyLink: false,
        };
      },
    }),
  ],
});
from typing import Any, Dict, Optional, Union

from supertokens_python.recipe import accountlinking
from supertokens_python.recipe.accountlinking.types import (
    AccountInfoWithRecipeIdAndUserId,
    ShouldAutomaticallyLink,
    ShouldNotAutomaticallyLink,
)
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.types import 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 user is None:
        return ShouldAutomaticallyLink(should_require_verification=True)

    if (
        session is not None
        and session.get_user_id() == user.id
        and session.get_tenant_id() == tenant_id
    ):
        return ShouldAutomaticallyLink(should_require_verification=True)

    return ShouldNotAutomaticallyLink()


accountlinking.init(
    should_do_automatic_account_linking=should_do_automatic_account_linking
)

The callback allows a new user to become a primary user when user is absent. It links to an existing user only when the session user and tenant match the proposed primary user and current tenant. It therefore does not enable linking between existing users during first-factor authentication. To enable that behavior, see the automatic account linking page.

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 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. 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 (> ERR_CODE_008).

3. Access the social login access token and user profile 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:

import SuperTokens, { User } from "supertokens-node";
import ThirdParty from "supertokens-node/recipe/thirdparty";
import Session from "supertokens-node/recipe/session";

SuperTokens.init({
  appInfo: {
    apiDomain: "...",
    appName: "...",
    websiteDomain: "...",
  },
  supertokens: {
    connectionURI: "...",
  },
  recipeList: [
    ThirdParty.init({
      override: {
        functions: (originalImplementation) => {
          return {
            ...originalImplementation,
            // override the thirdparty sign in / up function
            signInUp: async function (input) {
              let existingUser: User | undefined;
              if (input.session !== undefined && input.session.getTenantId() === input.tenantId) {
                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 &&
                  input.session.getTenantId() === input.tenantId &&
                  response.user.id === input.session.getUserId() &&
                  existingUser !== undefined
                ) {
                  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;
            },
          };
        },
      },
    }),
    Session.init({
      /* ... */
    }),
  ],
});
from typing import Any, Dict, Optional

from supertokens_python.recipe import thirdparty
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.recipe.thirdparty.interfaces import (
    RecipeInterface,
    SignInUpOkResult,
)
from supertokens_python.recipe.thirdparty.types import RawUserInfoFromProvider


def override_thirdparty_functions(
    original_implementation: RecipeInterface,
) -> 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: Optional[bool],
        tenant_id: str,
        user_context: Dict[str, Any],
    ):
        existing_login_method_count = None
        if session is not None and session.get_tenant_id() == tenant_id:
            from supertokens_python.asyncio import get_user

            existing_user = await get_user(session.get_user_id(), user_context)
            if existing_user is not None:
                existing_login_method_count = len(existing_user.login_methods)

        result = 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 (
            isinstance(result, SignInUpOkResult)
            and session is not None
            and session.get_tenant_id() == tenant_id
            and result.user.id == session.get_user_id()
            and existing_login_method_count is not None
        ):
            _access_token = result.oauth_tokens.get("access_token")
            _provider_profile = result.raw_user_info_from_provider.from_user_info_api

            if len(result.user.login_methods) == existing_login_method_count + 1:
                pass  # The provider account was linked to this session user.
            else:
                pass  # The provider account was already linked to this session user.

        return result

    original_implementation.sign_in_up = sign_in_up
    return original_implementation


thirdparty.init(
    override=thirdparty.ThirdPartyOverrideConfig(
        functions=override_thirdparty_functions
    )
)

The checks bind custom logic to the same session user and tenant. The provider identifiers, tokens, and profile in these function results come from the backend-verified OAuth exchange; never use client-submitted provider identifiers as proof that the current user owns a social account. A conflict leaves the provider login method linked to its existing primary user and returns a linking error instead of moving it to the session user.


See also

API reference

API schema and response details