supabase-swift icon indicating copy to clipboard operation
supabase-swift copied to clipboard

Link apple identity natively

Open MilesV64 opened this issue 1 year ago • 4 comments

Feature Request

We have native apple sign in, but no way (that I can see) of natively linking Apple as a sign in option to an existing account, we need to use the OAuth flow which redirects to web which is not a natural experience on an apple device for authenticating with apple.

Is it possible to skip the PKCE flow flor linking apple as an identity, authenticating natively?

MilesV64 avatar Nov 04 '24 16:11 MilesV64

Hi @MilesV64, unfortunately, that isn't supported yet. The only way to link an account is through the OAuth web flow.

We don't have an ETA for this, but it is on the roadmap, I'll keep this issue open for posting updates for this feature.

Thanks.

grdsdev avatar Nov 05 '24 10:11 grdsdev

Hi @MilesV64, I ran into a similar issue with my app. Users can sign in anonymously, but at some point they want to create an account and link all their contributions to this new account.

I was thinking about the following work-around without needed an OAuth web flow. It's far from perfect.. A native solution would be better but it doesn't exist yet... There will be issues, please let me know so I can try and resolve them!

One security issue is obvious: I can't verify that the old uuid actually belonged to this user since functions aren't stateful and I can only call it after the user has signed in with apple (or OAuth, ...) since I need to know their new uuid. If there are solutions to this please let me know!

My solution that seems to work in test environment:

I create a function in Supabase to merge their Profile (table with some more info about auth.users. Profiles has a uuid 'user_id' which is a foreign key into auth.users.id, and a uuid 'profile_id' which is used in all other tables as foreign key (when a user deletes their account I don't want to cascade and delete all their contributions, instead I anonymize them). Anyways back to the function below. I call this function from the users device when they successfully auth with apple and pass their auth.user.id from the anonymous user and from the authenticated user.

CREATE OR REPLACE FUNCTION public.merge_user_requested(old_uuid UUID, new_uuid UUID) 
RETURNS VOID 
SET search_path = ''
LANGUAGE plpgsql 
SECURITY DEFINER 
AS $$
DECLARE
    old_profile RECORD;
    caller_user UUID := COALESCE(auth.uid(), gen_random_uuid()); -- Get the caller's user ID or fallback to a random UUID
    old_user_is_anonymous BOOLEAN;
    old_profile_id UUID;
    new_profile_id UUID;
BEGIN
  -- Check if the user calling the function is the same as new_uuid
  IF caller_user IS DISTINCT FROM new_uuid THEN
    RAISE EXCEPTION 'Permission denied: You can only merge your own profiles.';
  END IF;

  -- Check if old_uuid is an anonymous account
  SELECT is_anonymous INTO old_user_is_anonymous 
  FROM auth.users 
  WHERE id = old_uuid;

  IF old_user_is_anonymous IS DISTINCT FROM TRUE THEN
    RAISE EXCEPTION 'The old account must be an anonymous account to proceed with the merge.';
  END IF;

  -- Get the profile_id of the old_uuid
  SELECT profile_id INTO old_profile_id
  FROM profiles
  WHERE user_id = old_uuid;

  -- Get the profile_id of the new_uuid
  SELECT profile_id INTO new_profile_id
  FROM profiles
  WHERE user_id = new_uuid;

  -- Delete the unused new profile
  DELETE FROM profiles WHERE user_id = new_uuid;

  -- Update the old profile with the new auth.user.id
  UPDATE profiles
  SET
      user_id = new_uuid,
      updated_at = NOW()
  WHERE user_id = old_uuid;

  -- Delete the old user from the auth.users table since it is a waste of space.
  DELETE FROM auth.users WHERE id = old_uuid;

END;
$$;
func linkAnonymousAccountToApple(result: Result<ASAuthorization, Error>) async throws {
        isLoading = true
        defer { isLoading = false }
        
        guard let credential: ASAuthorizationAppleIDCredential = try result.get().credential as? ASAuthorizationAppleIDCredential
        else {
            // Handle error
            return
        }
        
        guard let idToken = credential.identityToken
            .flatMap({ String(data: $0, encoding: .utf8) })
        else {
            // handle error
            return
        }
        // Save the anonymous (old) user id
        let old_uuid = user?.id
        
        let session = try await SupabaseModel.shared.client.auth.signInWithIdToken(
            credentials: .init(
                provider: .apple,
                idToken: idToken
            )
        )
        
        user = session.user
        
        // Unpack the old and new uuids and call the function using RPC
        if let oldUUID = old_uuid, let newUUID = user?.id {
            // Call the RPC with correctly named parameters
            let params: [String: UUID] = [
                "old_uuid": oldUUID,
                "new_uuid": newUUID
            ]
            
            // Add error handling here.
            let _ = try await SupabaseModel.shared.client.rpc("merge_user_requested", params: params).execute()
        }

RicoMontulet avatar Nov 27 '24 15:11 RicoMontulet

I'm currently stuck without this. Where can I learn about the "OAuth flow which redirects to web" approach?

paul-brenner avatar Dec 16 '24 21:12 paul-brenner

@paul-brenner

Using the swift client you can call (passing in your own redirect url ie your-domain.com/provider/apple:

try await supabaseClient.auth.linkIdentity(
    provider: .apple,
    redirectTo: redirect,
    launchURL: launchURL
)

Then launchURL is a closure and in your app you can launch a WKWebView and set the WKNavigationDelegate, then in func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) you check for the redirect url you passed and then get the session from the url via the supabase auth client again: try await supabaseClient.auth.session(from: url)

MilesV64 avatar Dec 16 '24 22:12 MilesV64

@paul-brenner

Using the swift client you can call (passing in your own redirect url ie your-domain.com/provider/apple:

try await supabaseClient.auth.linkIdentity( provider: .apple, redirectTo: redirect, launchURL: launchURL ) Then launchURL is a closure and in your app you can launch a WKWebView and set the WKNavigationDelegate, then in func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) you check for the redirect url you passed and then get the session from the url via the supabase auth client again: try await supabaseClient.auth.session(from: url)

Hi @MilesV64, Where do you set the redirect urls?

doganaltinbas avatar Apr 28 '25 07:04 doganaltinbas

Hi @grdsdev, would it be possible to provide any update on this? It seems to be a highly requested feature across frameworks, and the native UX is a clear improvement. Appreciate any info you can share. Thanks.

nikolouzos avatar May 23 '25 14:05 nikolouzos

Hey everyone,

We have an open PR (https://github.com/supabase/supabase-swift/pull/776) for adding support for native identity linking. Thanks a lot for your patience!

grdsdev avatar Aug 08 '25 12:08 grdsdev