Using @turnkey/sdk-react
Setting up social linking with @turnkey/sdk-react using the <Auth/>
component is straightforward.
Simply add the socialLinking
property to your authConfig
object and set it to true
. This will enable social linking for your application.
import { Auth } from "@turnkey/sdk-react";
function AuthPage() {
const handleAuthSuccess = async () => {
console.log("Auth successful!");
};
const handleAuthError = (errorMessage: string) => {
console.log("Error!");
};
const authConfig = {
emailEnabled: true,
passkeyEnabled: true,
phoneEnabled: false,
googleEnabled: true,
appleEnabled: false,
facebookEnabled: false,
socialLinking: true, // Enable social linking
};
const configOrder = ["socials", "email", "phone", "passkey"];
return (
<Auth
authConfig={authConfig}
configOrder={configOrder}
onAuthSuccess={handleAuthSuccess}
onError={handleAuthError}
/>
);
}
export default AuthPage;
You can view the implementation of the handleOAuthLogin
function within the Auth
component in the @turnkey/sdk-react GitHub repository.
Manual Implementation
If you prefer to implement social linking manually, you can follow this guide to create a backend handleOAuthLogin
function / endpoint that can optionally allow for social linking. Code references are in TypeScript, but you can easily adapt this to your backend language of choice.
Parameters
This function will accept the following parameters
oidcToken
: The OIDC token received from the social login provider.
providerName
: The name of the social login provider (e.g., “google”, “apple”, etc.).
publicKey
: A public key generated by the client.
socialLinking
: A boolean indicating whether social linking is enabled. Defaults to false
.
Parse the OIDC Token
Extract the email
and issuer
(iss
) from the user’s OIDC token.
import { jwtDecode } from "jwt-decode";
const { email, iss } =
jwtDecode<{ email?: string; iss?: string }>(oidcToken) || {};
Look Up Sub-Orgs by OIDC Token
Check if this OIDC token is already linked to a sub-org user within your organization. If it is, you can simply continue to the OAuth login step.
const { organizationIds: orgIdsOidc } = await turnkey.getSubOrgIds({
filterType: "OIDC_TOKEN",
filterValue: oidcToken,
});
if (orgIdsOidc.length > 0) {
organizationId = orgIdsOidc[0];
// Proceed to oauthLogin
}
If No OIDC Match and Google Issuer, Try Finding the Email
If the token is from Google and has a valid email, try getting the sub-org by verified email. If a sub-org is found, you can create the OAuth provider for the user. Note, this should only be done for social linking flows.
if (socialLinking && email && iss === "https://accounts.google.com") {
const { organizationIds: orgIdsEmail } = await turnkey.getVerifiedSubOrgIds({
filterType: "EMAIL",
filterValue: email,
});
if (orgIdsEmail.length > 0) {
organizationId = orgIdsEmail[0];
// Fetch the user ID in the sub-org
const { users } = await turnkey.getUsers({ organizationId });
const userId = users[0].userId; // If the sub-org has multiple users, you may want to handle this differently.
// Link the Google OIDC provider
await turnkey.createOauthProviders({
organizationId,
userId,
oauthProviders: [
{
providerName,
oidcToken,
},
],
});
}
}
If Still No Match, Create a New Sub-Organization
If no match is found, create a new sub-org and include the email in the payload for a social linking flow. The email passed in will be marked as verified provided the OIDC token comes from Google and the email within the token matches the email passed in.
const createSuborgParams: Record<string, any> = {
...(socialLinking && email && { email }),
oauthProviders: [{ providerName, oidcToken }],
};
const result = await handleCreateSubOrg(createSuborgParams);
organizationId = result.subOrganizationId;
Complete the OAuth Login
Finally, perform the OAuth login using the oidcToken
, publicKey
, and the determined organizationId
.
const oauthResponse = await turnkey.oauthLogin({
organizationId,
oidcToken,
publicKey,
expirationSeconds: 900, // Set the expiration time as needed
});
Final Implementation
Here’s the complete implementation of the handleOAuthLogin
function as well as a helper function to create a sub-organization (can also be implemented inline).
import { Turnkey } from "@turnkey/sdk-server";
import { jwtDecode } from "jwt-decode";
export const turnkeyConfig = {
apiBaseUrl: process.env.TURNKEY_API_URL ?? "",
defaultOrganizationId: process.env.ORGANIZATION_ID ?? "",
apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY ?? "",
apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY ?? "",
};
const turnkey = new Turnkey(turnkeyConfig).apiClient();
async function handleCreateSubOrg(params: Record<string, any>) {
const { email, oauthProviders } = params;
const result = await turnkey.createSubOrganization({
organizationId: turnkeyConfig.defaultOrganizationId,
subOrganizationName: "A social linking suborganization",
rootUsers: [
{
userName: "A suborganization user",
userEmail: email,
oauthProviders,
},
],
rootQuorumThreshold: 1,
wallet: {
walletName: "Default Wallet",
accounts: DEFAULT_ETHEREUM_ACCOUNTS,
},
});
return result;
}
async function handleOAuthLogin(
oidcToken: string,
providerName: string,
publicKey: string,
socialLinking = false
) {
let organizationId: string = turnkeyConfig.defaultOrganizationId;
const { email, iss } =
jwtDecode<{ email?: string; iss?: string }>(oidcToken) || {};
const oauthProviders = [{ providerName, oidcToken }];
const createSuborgParams: Record<string, any> = {
...(socialLinking && email && { email }),
oauthProviders,
};
const { organizationIds: orgIdsOidc } = await turnkey.getSubOrgIds({
filterType: "OIDC_TOKEN",
filterValue: oidcToken,
});
if (orgIdsOidc.length > 0) {
organizationId = orgIdsOidc[0];
} else if (socialLinking && email && iss === "https://accounts.google.com") {
// For Google, first check if there is an organization with the email
const { organizationIds: orgIdsEmail } = await turnkey.getVerifiedSubOrgIds(
{
filterType: "EMAIL",
filterValue: email,
}
);
if (orgIdsEmail.length > 0) {
organizationId = orgIdsEmail[0];
const { users } = await turnkey.getUsers({
organizationId,
});
const userId = users[0].userId;
await turnkey.createOauthProviders({
organizationId,
userId,
oauthProviders: [
{
providerName,
oidcToken,
},
],
});
} else {
const result = await handleCreateSubOrg(createSubOrgParams);
organizationId = result.subOrganizationId;
}
} else {
const result = await handleCreateSubOrg(createSubOrgParams);
organizationId = result.subOrganizationId;
}
const oauthResponse = await turnkey.oauthLogin({
organizationId,
oidcToken,
publicKey,
expirationSeconds: 900,
});
return oauthResponse;
}