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
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
signinupAPI, 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
signinupAPI 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.