7

I have implemented the Sign-In-With-Apple with Firebase. And I also have the functionality to delete a user. This is what I do:

  static Future<bool> deleteUser(BuildContext context) async {
    try {
      await BackendService().deleteUser(
        context,
      );

      await currentUser!.delete(); // <-- this actually deleting the user from Auth

      Provider.of<DataProvider>(context, listen: false).reset();

      return true;
    } on FirebaseException catch (error) {
      print(error.message);
      AlertService.showSnackBar(
        title: 'Fehler',
        description: error.message ?? 'Unbekannter Fehler',
        isSuccess: false,
      );
      return false;
    }
  }

As you can see I delete all the users data and finally the user himself from auth.

But Apple still thinks I am using the App. I can see it inside my Settings:

enter image description here

Also when trying to sign in again with apple, it acts like I already have an account. But I just deleted it and there is nothing inside Firebase that says that I still have that account? How can I completely delete an Apple user from Firebase? What am I missing here?

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
Chris
  • 1,828
  • 6
  • 40
  • 108

4 Answers4

2

Apple and some other 3rd party identity provider do not provide APIs to do so commonly.

Access to those data may lead to privacy issue, for e.g., a malicious app can remove the authorization information after access to user profile.

But if you want to do a "graceful" logout, you can ask your users to logout from iOS Settings, and listen to the server-to-server notification for revoking.

Chino Chang
  • 178
  • 6
1

Although users account has been deleted on firebase it has not been removed from Apple's system. At the time of writing firebase SDK for Apple is still working on this feature git hub issue (Planned for Q4 2022 or Q1 2023), as flutter and react native are probably dependant on base SDK a custom implementation is needed until this is available.

According to Apple, to completely remove users Apple account you should obtain Apple's refresh token using generate_tokens API and then revoke it using revoke_tokens API.

High level description:

  1. Client side (app): Obtain Apple authorization code.
  2. Send authorization code to your server.
  3. Server side: Use Apples p8 secret key to create jwt token. Jwt token will be used for authenticating requests towards Apple's API
  4. Server side: Trade authorization code for refresh_token (see first link above)
  5. Server side: Revoke refresh_token (see second link above)

Detailed description: https://stackoverflow.com/a/72656672/6357154

.NET implantation of the server side process. Assumptions:

  • _client is a HttpClient registered in DI contrainer with base url from Apple docs posted above
  • AppleClientOptions contains the same values used for Apple setup on firebase.
/// <summary>
/// Gets apple refresh token
/// SEE MORE: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
/// </summary>
/// <param name="jwtToken"></param>
/// <param name="authorizationCode"></param>
/// <returns></returns>
public async Task<string> GetTokenFromApple(string jwtToken, string authorizationCode)
{
    IEnumerable<KeyValuePair<string, string>> content = new[]
    {
        new KeyValuePair<string, string>("client_id", _appleClientOptions.ClientId),
        new KeyValuePair<string, string>("client_secret", jwtToken),
        new KeyValuePair<string, string>("code", authorizationCode),
        new KeyValuePair<string, string>("grant_type", "authorization_code"),
    };
    var encodedContent = new FormUrlEncodedContent(content);
    var response = await _client.PostAsync("auth/token", encodedContent);
    var responseAsString = await response.Content.ReadAsStringAsync();
    if (response.IsSuccessStatusCode)
    {
        var appleTokenResponse = JsonConvert.DeserializeObject<AppleTokenResponse>(responseAsString);
        return appleTokenResponse.refresh_token;
    }
    _logger.LogError($"GetTokenFromApple failed: {responseAsString}");
    return null;
}

/// <summary>
/// Revokes apple refresh token
/// SEE MORE: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
/// </summary>
/// <param name="jwtToken"></param>
/// <param name="refreshToken"></param>
/// <returns></returns>
public async Task<bool> RevokeToken(string jwtToken, string refreshToken)
{
    IEnumerable<KeyValuePair<string, string>> content = new[]
    {
        new KeyValuePair<string, string>("client_id", _appleClientOptions.ClientId),
        new KeyValuePair<string, string>("client_secret", jwtToken),
        new KeyValuePair<string, string>("token", refreshToken),
        new KeyValuePair<string, string>("token_type_hint", "refresh_token"),
    };
    var response = await _client.PostAsync("auth/revoke", new FormUrlEncodedContent(content));
    return response.IsSuccessStatusCode;
}

private string GenerateAppleJwtTokenLinux()
{
    var epochNow = (int) DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
    var (payload, extraHeaders) = CreateJwtPayload(
        epochNow, 
        _appleClientOptions.TeamId,
        _appleClientOptions.ClientId,
        _appleClientOptions.KeyId);
    
    var privateKeyCleaned = Base64Decode(_appleClientOptions.PrivateKey)
        .Replace("-----BEGIN PRIVATE KEY-----", string.Empty)
        .Replace("-----END PRIVATE KEY-----", string.Empty)
        .Replace("\r\n", string.Empty)
        .Replace("\r\n", string.Empty);
    var bytes = Convert.FromBase64String(privateKeyCleaned);
    
    using var ecDsaKey = ECDsa.Create();
    ecDsaKey!.ImportPkcs8PrivateKey(bytes, out _);
    
    return Jose.JWT.Encode(payload, ecDsaKey, JwsAlgorithm.ES256, extraHeaders);
}

private static (Dictionary<string, object> payload, Dictionary<string, object> extraHeaders) CreateJwtPayload(
    int epochNow,
    string teamId,
    string clientId,
    string keyId)
{
    var payload = new Dictionary<string, object>
    {
        {"iss", teamId},
        {"iat", epochNow},
        {"exp", epochNow + 12000},
        {"aud", "https://appleid.apple.com"},
        {"sub", clientId}
    };
    var extraHeaders = new Dictionary<string, object>
    {
        {"kid", keyId},
        {"alg", "ES256"}
    };
    return (payload, extraHeaders);
}


/// <summary>
/// https://developer.apple.com/documentation/sign_in_with_apple/tokenresponse
/// </summary>
public class AppleTokenResponse
{
    public string access_token { get; set; }
    
    public string expires_in { get; set; }
    
    public string id_token { get; set; }
    
    public string refresh_token { get; set; }
    
    public string token_type { get; set; }
}

public class AppleClientOptions
{
    public string TeamId { get; set; }

    public string ClientId { get; set; }

    public string KeyId { get; set; }
        
    public string PrivateKey { get; set; }
}
public async Task<bool> DeleteUsersAccountAsync(string appleAuthorizationCode)
{
    // Get jwt token:
    var jwtToken = _appleClient.GenerateAppleJwtTokenLinux(); // Apple client is code form above, registered in DI.
    // Get refresh token from authorization code:
    var refreshToken = await _appleClient.GetTokenFromApple(jwtToken, appleAuthorizationCode);
    if (string.IsNullOrEmpty(refreshToken)) return false;

    // Delete token:
    var isRevoked = await _appleClient.RevokeToken(jwtToken, refreshToken);
    _logger.LogInformation("Deleted apple tokens for {UserId}", userId);
    if (!isRevoked) return false;
    return true;
}

Other implementation examples:

Marko Prcać
  • 550
  • 7
  • 17
-1

You did actually delete the user from Firebase but Apple doesn't know about that. You should delete that information also from Apple. Open the Settings app on your iPhone, then tap on your name at the top. Then press "Password & Security", then "Apple ID logins". All Apple ID logins should be listed there and can be deleted.

Pragma
  • 77
  • 7
  • 1
    That‘s exactly my question: how can I delete the user from apple settings ? The user should not have to do that by themselves. – Chris Apr 19 '22 at 06:34
  • I don't think Apple will let you access the user's Apple ID settings over Firebase. The question asked in the link below as "unlinking the Apple ID" but no useful answers so far. https://developer.apple.com/forums/thread/691537 So the best solution might just be guiding users to delete their Apple ID logins themselves. – Pragma Apr 19 '22 at 10:14
-1

so... Apple does not provide this service. But I found a workaround.

My sign in process:

1. Check if user signed in before

  // Create an `OAuthCredential` from the credential returned by Apple.
  final oauthCredential = OAuthProvider("apple.com").credential(
    idToken: appleCredential.identityToken,
    rawNonce: rawNonce,
  );

  // If you can not access the email property in credential,
  // means that user already signed in with his appleId in the application once before
  bool isAlreadyRegistered = appleCredential.email == null;

Now to the crucial part:

2. sign in user and check if that uid already exists in Firebase

  final UserCredential result =
      await FirebaseAuth.instance.signInWithCredential(
    oauthCredential,
  );

  isAlreadyRegistered = await BackendService.checkIfUserIdExists(
    result.user?.uid ?? '',
  );

checkIfUserIdExists is quite simple as well:

  static Future<bool> checkIfUserIdExists(String userId) async {
    try {
      var collectionRef = FirebaseFirestore.instance.collection(
        BackendKeys.users,
      );

      var doc = await collectionRef.doc(userId).get();
      return doc.exists;
    } on FirebaseException catch (e) {
      return false;
    }
  }
Chris
  • 1,828
  • 6
  • 40
  • 108
  • So how can your answer solve `But Apple still thinks I am using the App`? – Chino Chang Apr 25 '22 at 12:50
  • @ChinoChang it's a workaround. In for my case the above solution works perfeclty fine. The user does not have to delete the Profile in settings. But deleting the app acocunt and signing in again works with this. – Chris Apr 25 '22 at 12:53