Link Multiple Auth Providers to an Account · Issue #313 · supabase/auth · GitHub
Skip to content

Link Multiple Auth Providers to an Account #313

Open

Description

@sandbox-apps

Activity

  1. rodjoseph commented on Jul 27, 2021

    @rodjoseph

    I think this would be great as well

  2. kiwicopple commented on Jul 28, 2021

    @kiwicopple
    Member

    Hey @sandbox-apps - this is technically possible, as long as the user has the same email for all accounts (since this is the primary login mechanism).

    For example

    Both of these will return the same user uuid.

    Is that what you're looking for?

  3. sandbox-apps commented on Aug 17, 2021

    @sandbox-apps
    Author

    Firebase has this setup where
    Example:

    1. I signed up via email/password then login on app
    2. Go to my profile then link social for example facebook
    3. Even though facebook has different email address it can be linked

    Another example:

    1. I signed up via email/password then login on app
    2. Go to my profile then link social for example twitter
    3. Even though twitter has no email address it can be linked (via uid and providerId) see here: Twitter OAuth signup fails if user has no email on their twitter account supabase#2853

    Result:

    1. I can sign in via email/password
    2. I can sign in via facebook and it will not create another account because it is already connected to a user even though they different email address returned during OAuth
    3. I can sign in via twitter and it will not create another account because it is already connected to a user even though twitter has no email address returned during OAuth

    Things also that we can keep in mind that..

    1. If a user signed up with email/password with an email of test@gmail.com, user can still create an separate account via facebook/twitter if this social has the same email.
    2. Unless link or merge, then it will create new account.
    3. You can only link account if there is a user that is currently logged in on app/system to determine where would you append the additional data
  4. sandbox-apps commented on Aug 17, 2021

    @sandbox-apps
    Author

    Further more you can unlink provider via providerId with the currentUser

    Firebase.auth.currentUser!!.unlink(providerId)
            .addOnCompleteListener(this) { task ->
                if (task.isSuccessful) {
                    // Auth provider unlinked from account
                    // ...
                }
            }
  5. eMeRiKa13 commented on Nov 4, 2021

    @eMeRiKa13

    I would love to have the workflow you describe @sandbox-apps!

    It seems the "perfect" way to manage auth with 3rd party providers.

  6. didavid61202 commented on Dec 6, 2021

    @didavid61202

    Hey @sandbox-apps - this is technically possible, as long as the user has the same email for all accounts (since this is the primary login mechanism).

    For example

    Both of these will return the same user uuid.

    Is that what you're looking for?

    Hi @kiwicopple, thanks for all your teams great work in supabase, lovin' it!
    Just want to mention that it would be great if we can link different provider to same account even if the email is different.
    Sometimes user use different email for different providers.
    And also able to unlink will be great if user want to have separated accounts event the email is the same.
    Thanks :)

  7. firatoezcan commented on Dec 16, 2021

    @firatoezcan

    For anyone wanting to support this, you'd need to propose to https://github.com/netlify/gotrue as that is where the logic behind users and providers live. I don't know if Supabase runs a fork of it, but this is where the request should be going to

  8. henningko commented on May 10, 2022

    @henningko

    Adding another use case here, besides multiple auth paths:
    I want to store connections to 3rd party apps, e.g. a Twitter connection for retrieving a user's tweets. Else, I need to duplicate a lot of the OAuth logic and store it separately.

  9. diogoribeirodev commented on Aug 14, 2022

    @diogoribeirodev

    Any updates on this

  10. kangmingtay commented on Sep 14, 2022

    @kangmingtay
    Member

    Hi everyone, we currently don't have plans to support manual linking of accounts in the near future. Currently, gotrue only supports automatic linking of accounts based on the user's email.

  11. jdgamble555 commented on Sep 28, 2022

    @jdgamble555

    This could be done theoretically with a procedure and policies that updates the auth.users and auth.identities tables.

    • You would basically delete the identity id in auth.identities, remove the provider in auth.users.raw_app_meta_data and auth.users.raw_user_meta_data.

    Adding it would require logging out and back in with the provider assuming the email is the same. If you wanted to take that a step further, you could create another procedure with an http function that runs the full oAuth 2 protocol, then saves it manually. You could also theoretically add the missing option for signin with popup.

    Of course, if someone does either of these two options, please share the code here.

    J

  12. hf commented on Sep 29, 2022

    @hf
    Contributor

    Hey everyone, we're aware of the need to customize the account linking logic and have some plans to make it happen via web hooks / triggers. However, this is not something we're going to be working on in the short term.

    I'll be closing this issue as account linking is supported if the user has the same email in the social login providers.

  13. rlee1990 commented on Feb 11, 2023

    @rlee1990

    I don't get the point of closing this and stating that account linking is suppoerted when its the same email but the point of this whole thread is to get support for when its NOT THE SAME EMAIL. Its a must have future in auth now and days. This should really be pushed up the priority list.

  14. 16 remaining items

  15. aaroniker commented on Nov 19, 2023

    @aaroniker

    We are working on it! Sorry for the delay... it's not a simple feat as we need to make sure not to break any existing behavior, while allowing for an API that we won't have to go back to in 5 months time.

    @hf Thanks for the update, that's awesome!! ✨ any rough ETA?

  16. kangmingtay commented on Dec 1, 2023

    @kangmingtay
    Member

    Hey everyone, just an update, we've merged in 2 PRs that will make manual linking possible very soon:

    1. Link an oauth identity to a user: feat: add manual linking APIs #1317
    2. Unlink an oauth identity: feat: add endpoint to unlink identity from user #1315

    We'll need some time to roll this feature out to all projects on Supabase but if you are self-hosting, you should be able to use this new feature with the latest version of gotrue. The target ETA for getting this out to all projects is by LWX

    The corresponding client library bindings will also be added to the js library in this PR: supabase/auth-js#814

    The process for linking / unlinking an identity will look something like this:

    // assume user is already logged in and authenticated
    
    // link the current user to a google identity
    // this works very similar to signInWithOAuth() and will redirect the user to google to complete the oauth flow 
    // once the oauth flow has been completed, the user will be redirected back to the app with the identity linked
    const { data, error } = await supabase.auth.linkIdentity({ provider: 'google' })
    
    // fetch all identities linked to the current user
    const { data: { identities } } = await supabase.auth.getUserIdentities()
    
    const googleIdentity = identities.find(identity => identity.provider === 'google')
    
    // unlink an identity
    await supabase.auth.unlinkIdentity(googleIdentity)
  17. ARMATAV commented on Dec 2, 2023

    @ARMATAV
  18. evelant commented on May 21, 2024

    @evelant

    @kangmingtay I'm migrating from Firebase Auth to Supabase Auth now that there is anonymous account support. I'm on react-native but the docs for linking seem to assume web. What is the correct auth API to use to link an anonymous account to a credential retrieved from a native social auth provider?

    For example the docs suggest using supabase.auth.linkIdentity({ provider: 'google' }) stating that "the user will be redirected to Google to complete the OAuth2.0 flow" which probably won't work on react native, it seems to be assuming web. Some of the provider documentation like google auth demonstrate signing in using a token fetched from the native auth flow (react-native-google-sign-in) using supabase.auth.signInWithIdToken but there is no mention of linking the credential to an anonymous account.

    The workflow I'm trying to replicate from Firebase auth is that new users always get an anonymous account and at some later point in time are prompted to link a real credential so they don't lose their progress. It seems like maybe the supabase client is missing a linkIdentityWithIdToken method?

  19. f-bog commented on Jul 2, 2024

    @f-bog

    @kangmingtay I'm migrating from Firebase Auth to Supabase Auth now that there is anonymous account support. I'm on react-native but the docs for linking seem to assume web. What is the correct auth API to use to link an anonymous account to a credential retrieved from a native social auth provider?

    For example the docs suggest using supabase.auth.linkIdentity({ provider: 'google' }) stating that "the user will be redirected to Google to complete the OAuth2.0 flow" which probably won't work on react native, it seems to be assuming web. Some of the provider documentation like google auth demonstrate signing in using a token fetched from the native auth flow (react-native-google-sign-in) using supabase.auth.signInWithIdToken but there is no mention of linking the credential to an anonymous account.

    The workflow I'm trying to replicate from Firebase auth is that new users always get an anonymous account and at some later point in time are prompted to link a real credential so they don't lose their progress. It seems like maybe the supabase client is missing a linkIdentityWithIdToken method?

    I'm in the same boat, and managed to get it to work using linkIdentity, however I'm using expo-web-browser and expo-auth-session in my app to achieve this.

            const { data, error } = await supabase.auth.linkIdentity({
              provider: "apple",
              options: {
                redirectTo,
              },
            });
            if (error) throw error;
    
            WebBrowser.openBrowserAsync(data.url);

    I feel like there's a bit of wonkiness to this though. If the anonymous user attempts to link to a google (or apple account) with one that is already used.. it seems to fail silently, and I'm not able to display any meaningful message to the client app.

    Edit: Just found the fix for my problem above, had to read the url from the Linking.addEventListener event.. My bad!

  20. lawinski commented on Sep 1, 2024

    @lawinski

    Thanks for implementing account linking! :) Referring to @evelant message and #1645 issue, I think it would be helpful to add support for linking accounts via id token. If someone has implemented mobile native login, they are currently forced to start the linking process in a web view. While this isn’t a major issue, native login offers a better user experience, and I’m sure many developers would prefer to keep things consistent in their apps. After looking at the code, it seems like linking accounts via an id token shouldn’t be too difficult, but it would be great if one of the maintainers could share their thoughts on whether this is a good approach or what the plans are for improving account linking.

  21. jonathanlab commented on Sep 10, 2024

    @jonathanlab

    Bumping a request for a linkIdentityWithIdToken function or similar functionality 👍

    This would perform the following action:

    • Take the existing (possibly anonymous) user
    • Link the provider to the existing user using an OIDC ID token for that provider

    My use case for this feature:

    • Currently I have an Apple OIDC ID token acquired using expo-apple-authentication
    • There's no way to link the Apple provider to the previously created anonymous user using this OIDC ID

    Attempted alternatives:

    • Using linkIdentity is not great since this requires the use of a WebView in the app instead of using the native auth implementation provided by expo-apple-authentication
    • signInWithIdToken creates an entirely new user, even if you're already signed in as an (anonymous) user, which is undesired for my use case
  22. lvxduck commented on Sep 21, 2024

    @lvxduck

    I am facing the same issue.
    I Hope Supabase team will add native Google and Apple sign-in to the linkIdentity soon.

  23. johndpope commented on Dec 23, 2024

    @johndpope

    @jonathanlab - drafted linkIdentityWithIdToken - #1885

  24. evelant commented on Jan 15, 2025

    @evelant

    @hf @kangmingtay 14 months ago:

    We are working on it! Sorry for the delay... it's not a simple feat as we need to make sure not to break any existing behavior, while allowing for an API that we won't have to go back to in 5 months time.

    Would you please let us know the status of this or review and merge #1885? Trying to work around this is painful.

  25. evelant commented on Jan 15, 2025

    @evelant

    Actually this appears to be a blocker for passing App Store review. Since we offer users the choice of using an anonymous account and then signing up later the inability to link with a token triggers an app store review failure. Apple will not accept supabase auth's default workflow of opening the default web browser to perform auth. They require use of the native sdk. This is not a problem for new signups because we can use expo-apple-authentication and create a new account with the token. This is a blocker to app store acceptance if using anonymous accounts. Right now there is no way to link an anonymous account to a token without using the web based oauth flow which apple will not accept through review.

  26. evelant commented on Jan 20, 2025

    @evelant

    For others waiting for gotrue to support linking to tokens, I realized once again that one of the great things about supabase is "it's just postgres". You can manually verify a token and insert an identity to link a token to an account in an edge function or similar server function. Here's how I'm doing it (contains some stuff specific to my app because I use effect-ts rpc on bun on fly.io to avoid the Deno woes of edge-functions but should be enough to go on). I looked at what gotrue was doing and the auth schema and figured out what data I needed to insert. Works like a charm!

    import { createHash } from "crypto"
    import { Effect } from "effect"
    import * as jose from "jose"
    import { RpcLinkAnonymousWithApple } from "myapp/schemas/RPCSchema"
    import { AuthTokenValidationError, InvalidArgsError } from "myapp/utils/errors"
    import { Session } from "../../index"
    import { runEffectWithTransaction, runSql } from "../dbEffect"
    
    interface ApplePublicKey {
        kty: string
        kid: string
        use: string
        alg: string
        n: string
        e: string
    }
    
    interface ApplePublicKeyResponse {
        keys: ApplePublicKey[]
    }
    
    interface AppleIDTokenPayload {
        iss: string
        sub: string
        aud: string
        iat: number
        exp: number
        email?: string
        email_verified?: boolean
        is_private_email?: boolean
        auth_time?: number
        nonce?: string
        name?: string
    }
    
    function createIdentityData(payload: AppleIDTokenPayload) {
        // Ensure we match GoTrue's expected structure
        const identityData = {
            sub: payload.sub,
            iss: payload.iss,
            aud: Array.isArray(payload.aud) ? (payload.aud as string[]) : [payload.aud],
            iat: payload.iat,
            exp: payload.exp,
            email: payload.email ?? null,
            email_verified: payload.email_verified ?? false,
            is_private_email: payload.is_private_email ?? null,
            auth_time: payload.auth_time ?? null,
            provider: "apple",
            provider_id: payload.sub,
            name: payload.name ?? null,
            full_name: payload.name ?? null,
            avatar_url: null
        } as const
    
        // Validate required fields
        if (!identityData.sub) throw new Error("Missing sub claim")
        if (!identityData.iss) throw new Error("Missing iss claim")
        if (!identityData.aud) throw new Error("Missing aud claim")
    
        return identityData
    }
    
    const sha256 = (str: string) => {
        return createHash("sha256").update(str).digest("hex")
    }
    
    export const linkAnonymousWithApple_V0 = ({ idToken, nonce }: RpcLinkAnonymousWithApple) =>
        Effect.gen(function* (_) {
            // Get the anonymous user's ID from the auth token
            const session = yield* _(Session)
            const anonymousUserId = session.uid
    
            // 1. Fetch Apple's public keys
            const appleKeysResponse = yield* _(
                Effect.tryPromise({
                    try: () => fetch("https://appleid.apple.com/auth/keys"),
                    catch: error => new InvalidArgsError({ cause: error })
                })
            )
            const appleKeys: ApplePublicKeyResponse = yield* _(
                Effect.tryPromise({
                    try: () => appleKeysResponse.json(),
                    catch: error => new InvalidArgsError({ cause: error })
                })
            )
    
            // 2. Decode the JWT header to get the key ID (kid)
            const decodedHeader = jose.decodeProtectedHeader(idToken)
            const matchingKey = appleKeys.keys.find(k => k.kid === decodedHeader.kid)
    
            if (!matchingKey) {
                return yield* _(Effect.fail(new InvalidArgsError({ msg: "No matching key found for token" })))
            }
    
            // 3. Convert Apple's JWK to PEM format and verify the token
            const publicKey = yield* _(
                Effect.tryPromise({
                    try: () => jose.importJWK(matchingKey),
                    catch: error => new InvalidArgsError({ cause: error })
                })
            )
    
            // 4. Verify the token
            const verifyOptions: jose.JWTVerifyOptions = {
                issuer: "https://appleid.apple.com",
                algorithms: ["RS256"],
                audience: "your.client.id.here",
                maxTokenAge: "6m",
                ...(nonce && { nonce: sha256(nonce) })
            }
    
            const verified = yield* _(
                Effect.tryPromise({
                    try: () => jose.jwtVerify(idToken, publicKey, verifyOptions),
                    catch: error => new AuthTokenValidationError({ cause: error })
                })
            )
    
            if (!verified || !verified.payload.sub) {
                return yield* _(Effect.fail(new InvalidArgsError({ msg: "Invalid token" })))
            }
    
            // 5. Start transaction to link accounts
            const identityResult = yield* _(
                runSql("linkAnonymousWithApple", sql => {
                    const appleId = verified.payload.sub!
                    const identityDataJson = createIdentityData(verified.payload as AppleIDTokenPayload)
                    return sql`
                        WITH user_check AS (
                            SELECT id, is_anonymous 
                            FROM auth.users 
                            WHERE id = ${anonymousUserId}::uuid
                            AND is_anonymous = true
                            FOR UPDATE
                        )
                        INSERT INTO auth.identities (
                            provider_id,
                            user_id,
                            identity_data,
                            provider,
                            last_sign_in_at,
                            created_at,
                            updated_at
                        )
                        SELECT 
                            ${appleId}::text,
                            ${anonymousUserId}::uuid,
                            ${identityDataJson as any}::jsonb,
                            'apple'::text,
                            NOW(),
                            NOW(),
                            NOW()
                        FROM user_check
                        WHERE is_anonymous = true
                        RETURNING id;
                    ` as any
                })
            )
    
            // 6. Update user's anonymous status and metadata
            const userResult = yield* _(
                runSql("updateUserAfterAppleLinking", sql => {
                    const email = typeof verified.payload.email === "string" ? verified.payload.email : ""
                    const emailConfirmedAt = verified.payload.email_verified ? new Date() : null
                    return sql`
                        UPDATE auth.users 
                        SET 
                            is_anonymous = false,
                            email = NULLIF(${email}::text, '')::text,
                            email_confirmed_at = COALESCE(${emailConfirmedAt}, NULL)::timestamptz,
                            updated_at = NOW()
                        WHERE id = ${anonymousUserId}::uuid
                        AND is_anonymous = true
                        RETURNING id;
                    ` as any
                })
            )
    
            return { success: true }
        }).pipe(runEffectWithTransaction)
  27. XanderZhu commented on May 8, 2025

    @XanderZhu

    @kangmingtay
    We really need a linkIdentityWithIdToken for linking google/apple account with anonymous user via ID token.
    Current linkIdentity witch use browser way is very inconvenient for mobile apps.
    signInWith(IDToken) already allows native sign in for mobile apps.

    JFYI: in our project we decided to remove sing-in functionality, because browser way is not suitable for us. Waiting for linkIdentityWithIdToken support.

  28. johndpope commented on May 8, 2025

    @johndpope

    PR + Code is here - (previous or got blown away using github ) #2002

  29. evelant commented on Jun 25, 2026

    @evelant

    This issue has been open for nearly 5 years. It seems it got partially implemented but there are a lot of outstanding issues and missing cases, especially linkIdentityWithIdToken.

    This is a real pain point, especially for those of us with react native apps. If you use supabase and want to offer social auth in a react native app you will be rejected by Apple app store review if you try to use the documented method. You must implement a totally custom solution like I outlined above to pass review.

    IMO it should at least be documented that "Social auth is not supported on react-native iOS via supabase-js. If you need social auth on react-native iOS you must implement token handling and auth insert manually on your backend".

    @kangmingtay any chance this can finally get some attention to fill out the edge cases, improve the docs, and resolve the rough edges mentioned in supabase/auth-js#814 so that auth can finally work well on all platforms?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions