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:
- Client side (app): Obtain Apple authorization code.
- Send authorization code to your server.
- Server side: Use Apples p8 secret key to create jwt token. Jwt token will be used for authenticating requests towards Apple's API
- Server side: Trade authorization code for refresh_token (see first link above)
- 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: