Linking multiple social providers to user/account #1235

Closed
opened 2026-03-13 08:29:30 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @tkrebs2 on GitHub (May 19, 2025).

Is this suited for github?

  • Yes, this is suited for github

The goal is to link one or more of the same social provider to a user.. So I sign-up / login-in credentials or whatever social provider - the user then can choose what other providers they'd grant access to and should be able to call .linkSocial()

  const handleTwitterLink = async () => {
    await authClient.linkSocial(
      {
        provider: "twitter",
        callbackURL: "/dashboard/account/connected-apps",
      },
      {
        onRequest: () => {
          console.log("onRequest");
        },
        onSuccess: (ctx) => {
          toast.success("Twitter connected successfully");
        },
        onError: (ctx) => {
          toast.error("Failed to connect Twitter account.");
        },
      }
    );
  };

and have that social provider be linked to the user - not create a new login method. I want to update the account row with the email, username, etc of that provider.. So if you see below I have linked a twitter account.

Image

Describe the solution you'd like

Ideally you should be able to call something like mapProfileToAccount and have it update the record with the data that comes from a given oauth provider..mapProfileToUser is overwriting the credential account which is not helpful here.

  socialProviders: {
    twitter: {
      clientId: process.env.TWITTER_CLIENT_ID!,
      clientSecret: process.env.TWITTER_CLIENT_SECRET!,
      redirectUri: "/dashboard/account/connected-apps",
      scope: [
        "users.read",
        "tweet.read",
        "tweet.write",
        "follows.read",
        "media.write",
        "offline.access",
        "users.email",
      ],
      mapProfileToAccount: (profile: any) => {
        console.log("Twitter Profile Data", profile.data);
        { /* actual data returned when this runs
         twitter profile data {
                id: '2346236236234623',
                profile_image_url: 'https://pbs.twimg.com/profile_images/23523523525/image.jpg',
                username: 'username2',
                name: 'Actual  Name',
                email: 'random.email@gmail.com'
              }
        */ }
        return {
          email: profile.email,
          // Where twitterUsername is an additionalField on Account
          username: profile.twitterUsername,
          image: profile.profile_image_url,
        };
      },

Describe alternatives you've considered

I've tried getUserInfo to use the accessToken to call the twitter api endpoint and return the data and update that way, but haven't had much luck here.

        twitter: {
            clientId: process.env.TWITTER_CLIENT_ID as string,
            clientSecret: process.env.TWITTER_CLIENT_SECRET as string,
            scope: ["users.email", "users.read", "tweet.read", "tweet.write", "follows.read", "media.write", "offline.access"],
            getUserInfo: async ({ accessToken }) => {
                const response = await fetch(
                    'https://api.twitter.com/2/users/me?user.fields=confirmed_email,profile_image_url,name,username,verified,verified_type',
                    { headers: { 'Authorization': `Bearer ${accessToken}` } }
                );

                if (!response.ok) throw new Error('Failed to fetch user info from Twitter');

                const data = await response.json();
                return {

                    user: {
                        id: data.data.id,
                        email: data.data.confirmed_email,
                        name: data.data.name,
                        image: data.data.profile_image_url,
                        username: data.data.username,
                        emailVerified: true,
                    },
                    data: data.data
                };
            },
        },

I've also tried updating the database directly with upsert via prisma.. which feels very hacky and leads to some odd issues..

Additional context

For context.. I'm attempting to add multiple social accounts that have read/write permissions so tweets, posts, images, etc can be posted from my application.

Originally created by @tkrebs2 on GitHub (May 19, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### Is your feature request related to a problem? Please describe. The goal is to link one or more of the same social provider to a user.. So I sign-up / login-in credentials or whatever social provider - the user then can choose what other providers they'd grant access to and should be able to call `.linkSocial()` ``` const handleTwitterLink = async () => { await authClient.linkSocial( { provider: "twitter", callbackURL: "/dashboard/account/connected-apps", }, { onRequest: () => { console.log("onRequest"); }, onSuccess: (ctx) => { toast.success("Twitter connected successfully"); }, onError: (ctx) => { toast.error("Failed to connect Twitter account."); }, } ); }; ``` and have that social provider be linked to the user - not create a new login method. I want to update the `account` row with the `email`, `username`, etc of that provider.. So if you see below I have linked a twitter account. ![Image](https://github.com/user-attachments/assets/56c5c7a5-2b79-4692-81d3-c99ef43b1f6d) ### Describe the solution you'd like Ideally you should be able to call something like `mapProfileToAccount` and have it update the record with the data that comes from a given oauth provider..`mapProfileToUser` is overwriting the credential account which is not helpful here. ``` socialProviders: { twitter: { clientId: process.env.TWITTER_CLIENT_ID!, clientSecret: process.env.TWITTER_CLIENT_SECRET!, redirectUri: "/dashboard/account/connected-apps", scope: [ "users.read", "tweet.read", "tweet.write", "follows.read", "media.write", "offline.access", "users.email", ], mapProfileToAccount: (profile: any) => { console.log("Twitter Profile Data", profile.data); { /* actual data returned when this runs twitter profile data { id: '2346236236234623', profile_image_url: 'https://pbs.twimg.com/profile_images/23523523525/image.jpg', username: 'username2', name: 'Actual Name', email: 'random.email@gmail.com' } */ } return { email: profile.email, // Where twitterUsername is an additionalField on Account username: profile.twitterUsername, image: profile.profile_image_url, }; }, ``` ### Describe alternatives you've considered I've tried `getUserInfo` to use the `accessToken` to call the twitter api endpoint and return the data and update that way, but haven't had much luck here. ``` twitter: { clientId: process.env.TWITTER_CLIENT_ID as string, clientSecret: process.env.TWITTER_CLIENT_SECRET as string, scope: ["users.email", "users.read", "tweet.read", "tweet.write", "follows.read", "media.write", "offline.access"], getUserInfo: async ({ accessToken }) => { const response = await fetch( 'https://api.twitter.com/2/users/me?user.fields=confirmed_email,profile_image_url,name,username,verified,verified_type', { headers: { 'Authorization': `Bearer ${accessToken}` } } ); if (!response.ok) throw new Error('Failed to fetch user info from Twitter'); const data = await response.json(); return { user: { id: data.data.id, email: data.data.confirmed_email, name: data.data.name, image: data.data.profile_image_url, username: data.data.username, emailVerified: true, }, data: data.data }; }, }, ``` I've also tried updating the database directly with `upsert` via prisma.. which feels very hacky and leads to some odd issues.. ### Additional context For context.. I'm attempting to add multiple social accounts that have read/write permissions so tweets, posts, images, etc can be posted from my application.
Author
Owner

@tkrebs2 commented on GitHub (May 20, 2025):

If I use signIn.Social function

const signIn = async () => {
    const data = await authClient.signIn.social({
        provider: "twitter"
    })
}

This creates a new user row which I don't want. It overwrites the current user. I should be able to call .linkSocial() and be able to update the new account row with the necessary info from the provider.

@tkrebs2 commented on GitHub (May 20, 2025): If I use `signIn.Social` function ``` const signIn = async () => { const data = await authClient.signIn.social({ provider: "twitter" }) } ``` This creates a new user row which I don't want. It overwrites the current user. I should be able to call `.linkSocial()` and be able to update the new account row with the necessary info from the provider.
Author
Owner

@azaek commented on GitHub (Jun 3, 2025):

I'm using this workaround to get the twitter/X username post account linking

export const auth = betterAuth({
  ...
    user: {
        additionalFields: {
            x_username: {
                type: "string",
                required: false,
            }
        }
    },
    socialProviders: {
        twitter: {
            clientId: process.env.TWITTER_CLIENT_ID as string,
            clientSecret: process.env.TWITTER_CLIENT_SECRET as string,
        }
    },
    databaseHooks: {
        account: {
            create: {
                after: async (account) => {
                    if (account.providerId === "twitter") {
                        const { data: profile, error: profileError } =
                            await betterFetch<TwitterProfile>(
                                "https://api.x.com/2/users/me",
                                {
                                    method: "GET",
                                    headers: {
                                        Authorization: `Bearer ${account.accessToken}`,
                                    },
                                },
                            );

                        console.log("res x api", profile, profileError);
                        await prisma.$transaction([
                            prisma.user.update({
                                where: {
                                    id: account.userId,
                                },
                                data: {
                                    x_username: profile?.data.username,
                                }
                            })
                            ...
                        ])
                    }
                },
            }
        }
    },
});

and then on client side

const { data } = authClient.useSession();

data.user.x_username reflects the changes right away

@azaek commented on GitHub (Jun 3, 2025): I'm using this workaround to get the twitter/X username post account linking ```ts export const auth = betterAuth({ ... user: { additionalFields: { x_username: { type: "string", required: false, } } }, socialProviders: { twitter: { clientId: process.env.TWITTER_CLIENT_ID as string, clientSecret: process.env.TWITTER_CLIENT_SECRET as string, } }, databaseHooks: { account: { create: { after: async (account) => { if (account.providerId === "twitter") { const { data: profile, error: profileError } = await betterFetch<TwitterProfile>( "https://api.x.com/2/users/me", { method: "GET", headers: { Authorization: `Bearer ${account.accessToken}`, }, }, ); console.log("res x api", profile, profileError); await prisma.$transaction([ prisma.user.update({ where: { id: account.userId, }, data: { x_username: profile?.data.username, } }) ... ]) } }, } } }, }); ``` and then on client side ```ts const { data } = authClient.useSession(); ``` `data.user.x_username` reflects the changes right away
Author
Owner

@dosubot[bot] commented on GitHub (Sep 2, 2025):

Hi, @tkrebs2. I'm Dosu, and I'm helping the better-auth team manage their backlog and am marking this issue as stale.

Issue Summary:

  • You requested a feature to link multiple social providers to a single user account without creating new login entries.
  • The current mapProfileToUser function overwrites credential accounts, which is not the desired behavior.
  • You proposed a mapProfileToAccount function or a .linkSocial() method to update user details from OAuth providers without creating new user rows.
  • A workaround was shared involving database hooks to update user fields after account linking.
  • The issue remains unresolved with no official feature implemented yet.

Next Steps:

  • Please let me know if this feature is still relevant to the latest version of better-auth by commenting on this issue.
  • If I do not hear back within 7 days, I will automatically close this issue.

Thank you for your understanding and contribution!

@dosubot[bot] commented on GitHub (Sep 2, 2025): Hi, @tkrebs2. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog and am marking this issue as stale. **Issue Summary:** - You requested a feature to link multiple social providers to a single user account without creating new login entries. - The current `mapProfileToUser` function overwrites credential accounts, which is not the desired behavior. - You proposed a `mapProfileToAccount` function or a `.linkSocial()` method to update user details from OAuth providers without creating new user rows. - A workaround was shared involving database hooks to update user fields after account linking. - The issue remains unresolved with no official feature implemented yet. **Next Steps:** - Please let me know if this feature is still relevant to the latest version of better-auth by commenting on this issue. - If I do not hear back within 7 days, I will automatically close this issue. Thank you for your understanding and contribution!
Author
Owner

@qweered commented on GitHub (Sep 9, 2025):

Still relevant

@qweered commented on GitHub (Sep 9, 2025): Still relevant
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1235