-
Notifications
You must be signed in to change notification settings - Fork 396
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Link Multiple Auth Providers to an Account #313
Comments
I think this would be great as well |
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 Is that what you're looking for? |
Firebase has this setup where
Another example:
Result:
Things also that we can keep in mind that..
|
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
// ...
}
} |
I would love to have the workflow you describe @sandbox-apps! It seems the "perfect" way to manage auth with 3rd party providers. |
Hi @kiwicopple, thanks for all your teams great work in supabase, lovin' it! |
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 |
Adding another use case here, besides multiple auth paths: |
Any updates on this |
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. |
This could be done theoretically with a procedure and policies that updates the
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 |
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. |
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. |
This is really a necessity for auth to be functional in a lot of cases. Automatic linking is too unpredictable and doesn't address the issues described here. |
Hi @rlee1990 and @sdaoud, I've pinned this issue to emphasise that this is definitely one of our top priorities going forward and we're working to make this happen gradually. If you've been keeping up with the updates on Supabase and gotrue, you may already know that automatic linking has always been the default way that gotrue links accounts. While we understand the advantages of manual linking that have been discussed in this thread, it's important to keep in mind that removing automatic linking altogether could create backward compatibility issues for existing users. Furthermore, we've received feedback from many developers who appreciate the simplicity of automatic linking for UX purposes. While we recognise the benefits of manual linking, it's not something that we can expect everyone to switch to overnight. I also want to reiterate that we'll eventually move to the model of manual linking accounts through a well-designed API. Currently, we are considering a few ideas to implement this:
@hf has already made some progress to simplify the account linking logic in this PR to make way for the use of webhooks to decide linking behaviour. Please feel free to drop any ideas you may have regarding this topic too as the team will be more than happy to discuss them in detail! |
@kangmingtay using a webhook sounds like a nice approach. I get keeping automatic linking in place also. A webhook to help link an account is a useful option. |
@jdgamble555 we thought of that option too but it's not feasible to make it toggle-able since automatic linking requires the email to be unique for the algorithm to know which user to link the new identity to. For example: (assuming
It has to be a one-way toggle or an option you select before you start building your app. Either you opt-in for automatic linking or resign to a manual linking API for eternity 🙃 |
Hmmm, I definitely don’t like the eternity issue. 🤷🏻 Perhaps the simple answer is to automatically link with the first found profile, in reality the first document id record containing that email. Any new profiles going forward would be automatic, but all profiles would have a link and unlink option after sign up. This puts the control back into the developer. So that boolean option is only concerning new signups, and the first found option. This solves any backwards compatible problems. Linking is always available. I should add this is also relevant for anonymous logins too, which is an entirely different feature. 😃 |
@kangmingtay @hf Can you explain what modifications are needed to be done in the database to link two accounts together? |
Hello, we need to achieve something similar. We have an app where the users, logged by social logins or not, could add other auth providers accounts with additional scope. The other accounts could be from the same auth provider. Example: I'm logged in my Sales CRM as [email protected] with access to my contacts, then I want to import contacts from my other gmail account named [email protected] without creating another account but linking the latter to the first. As a quick (and dirty?) workaround, we have overriden Gotrue callback URI to point out to our backend. This way, we could either fetch token and do our business or redirect users back to Gotrue for user account creation. Is it something planned to implement? |
this is very important to use as well ... would help cross-device sing-ins ... for ex if i sign up / sign in with apple ... i will not be able to use that account in a non-ios enviroment |
is there an estimation when this will be addressed? |
This seems to be a must-have feature for any auth solution. What site nowadays doesn't even let you connect multiple oauth providers with different emails? |
Ah this just managed to kick me off of using the auth system in Supabase for a project; really wish a |
@hf any update on this? 🙏🙂 |
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? |
Hey everyone, just an update, we've merged in 2 PRs that will make manual linking possible very soon:
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) |
This comment was marked as off-topic.
This comment was marked as off-topic.
@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 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 |
I'm in the same boat, and managed to get it to work using linkIdentity, however I'm using 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! |
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. |
Bumping a request for a This would perform the following action:
My use case for this feature:
Attempted alternatives:
|
I am facing the same issue. |
@JonathanLab - drafted linkIdentityWithIdToken - #1885 |
@hf @kangmingtay 14 months ago:
Would you please let us know the status of this or review and merge #1885? Trying to work around this is painful. |
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. |
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) |
https://firebase.google.com/docs/auth/android/account-linking
The text was updated successfully, but these errors were encountered: