skip to Main Content

Apple is complaining about my app because I am not calling the rest endpoint revoke token to delete an account.
I have to do it as described in this documentation: https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens

To call I need to get the client_id, client_secret and token.
The login process in my App is managed by Firebase and I don’t save this information when the user executes a login.
So I need to recover these 3 parameters from Firebase auth on IOS to call that revoke token endpoint.

There may be a method in the Firebase auth API on IOS that calls the Apple endpoint revoke_token for me and I am not seeing it.

3

Answers


  1. apple-token-revoke-in-firebase

    This document describes how to revoke the token of Sign in with Apple in the Firebase environment.
    In accordance with Apple’s review guidelines, apps that do not take action by June 30, 2022 may be removed.
    A translator was used to write this document, so I apologize whenever you feel weird about these sentences and describes.
    This document uses Firebase’s Functions, and if Firebase provides related function in the future, I recommend using it.

    The whole process is as follows.

    1. Get authorizationCode from App where user log in.
    2. Get a refresh token with no expiry time using authorizationCode with expiry time.
    3. After saving the refresh token, revoke it when the user leaves the service.

    You can get a refresh token at https://appleid.apple.com/auth/token and revoke at https://appleid.apple.com/auth/revoke.

    Getting started

    If you have implemented Apple Login using Firebase, you should have ASAuthorizationAppleIDCredential somewhere in your project.
    In my case, it is written in the form below.

      func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
          guard let nonce = currentNonce else {
            fatalError("Invalid state: A login callback was received, but no login request was sent.")
          }
          guard let appleIDToken = appleIDCredential.identityToken else {
            print("Unable to fetch identity token")
            return
          }
          guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
            print("Unable to serialize token string from data: (appleIDToken.debugDescription)")
            return
          }
          // Initialize a Firebase credential.
          let credential = OAuthProvider.credential(withProviderID: "apple.com",
                                                    IDToken: idTokenString,
                                                    rawNonce: nonce)
          // Sign in with Firebase.
          Auth.auth().signIn(with: credential) { (authResult, error) in
            if error {
              // Error. If error.code == .MissingOrInvalidNonce, make sure
              // you're sending the SHA256-hashed nonce as a hex string with
              // your request to Apple.
              print(error.localizedDescription)
              return
            }
            // User is signed in to Firebase with Apple.
            // ...
          }
        }
      }
    

    What we need is the authorizationCode. Add the following code under guard where you get the idTokenString.

    ...
    
    guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
      print("Unable to serialize token string from data: (appleIDToken.debugDescription)")
      return
    }
    
    // Add new code below
    if let authorizationCode = appleIDCredential.authorizationCode,
       let codeString = String(data: authorizationCode, encoding: .utf8) {
        print(codeString)
    }
    
    ...
    
    

    Once you get this far, you can get the authorizationCode when the user log in.
    However, we need to get a refresh token through authorizationCode, and this operation requires JWT, so let’s do this with Firebase functions.
    Turn off Xcode for a while and go to your code in Firebase functions.
    If you have never used functions, please refer to https://firebase.google.com/docs/functions.

    In Firebase functions, you can use JavaScript or TypeScript, for me, I used JavaScript.

    First, let’s declare a function that creates a JWT globally. Install the required packages with npm install.
    There is a place to write route of your key file and ID(Team, Client, Key), so plz write your own information.
    If you do not know your ID information, please refer to the relevant issue. https://github.com/jooyoungho/apple-token-revoke-in-firebase/issues/1

    function makeJWT() {
    
      const jwt = require('jsonwebtoken')
      const fs = require('fs')
    
      // Path to download key file from developer.apple.com/account/resources/authkeys/list
      let privateKey = fs.readFileSync('AuthKey_XXXXXXXXXX.p8');
    
      //Sign with your team ID and key ID information.
      let token = jwt.sign({ 
      iss: 'YOUR TEAM ID',
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + 120,
      aud: 'https://appleid.apple.com',
      sub: 'YOUR CLIENT ID'
      
      }, privateKey, { 
      algorithm: 'ES256',
      header: {
      alg: 'ES256',
      kid: 'YOUR KEY ID',
      } });
      
      return token;
    }
    

    The above function is returned by creating JWT based on your key information.
    Now, let’s get the Refresh token with AuthorizationCode.
    We will add a function called getRefreshToken to functions.

    exports.getRefreshToken = functions.https.onRequest(async (request, response) => {
    
        //import the module to use
        const axios = require('axios');
        const qs = require('qs')
    
        const code = request.query.code;
        const client_secret = makeJWT();
    
        let data = {
            'code': code,
            'client_id': 'YOUR CLIENT ID',
            'client_secret': client_secret,
            'grant_type': 'authorization_code'
        }
        
        return axios.post(`https://appleid.apple.com/auth/token`, qs.stringify(data), {
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        })
        .then(async res => {
            const refresh_token = res.data.refresh_token;
            response.send(refresh_token);
            
        });
    
    });
    

    When you call the above function, you get the code from the query and get a refresh_token.
    For code, this is the authorizationCode we got from the app in the first place.
    Before connecting to the app, let’s add a revoke function as well.

    
    exports.revokeToken = functions.https.onRequest( async (request, response) => {
    
      //import the module to use
      const axios = require('axios');
      const qs = require('qs');
    
      const refresh_token = request.query.refresh_token;
      const client_secret = makeJWT();
    
      let data = {
          'token': refresh_token,
          'client_id': 'YOUR CLIENT ID',
          'client_secret': client_secret,
          'token_type_hint': 'refresh_token'
      };
    
      return axios.post(`https://appleid.apple.com/auth/revoke`, qs.stringify(data), {
          headers: {
              'Content-Type': 'application/x-www-form-urlencoded'
          },
      })
      .then(async res => {
          console.log(res.data);
      });
    });
    
    

    The above function revokes the login information based on the refresh_token we got.
    So far we have configured our functions, and when we do ‘firebase deploy functions’ we will have something we added to the Firebase functions console.

    img

    Now back to Xcode.
    Call the Functions address in the code you wrote earlier to save Refresh token.
    I saved it in UserDefaults, You can save it in the Firebase database.

    ...
    
    // Add new code below
    if let authorizationCode = appleIDCredential.authorizationCode, let codeString = String(data: authorizationCode, encoding: .utf8) {
                  
          let url = URL(string: "https://YOUR-URL.cloudfunctions.net/getRefreshToken?code=(codeString)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "https://apple.com")!
                
            let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
                
                if let data = data {
                    let refreshToken = String(data: data, encoding: .utf8) ?? ""
                    print(refreshToken)
                    UserDefaults.standard.set(refreshToken, forKey: "refreshToken")
                    UserDefaults.standard.synchronize()
                }
            }
          task.resume()
          
      }
    
    ...
    
    

    At this point, the user’s device will save the refresh_token as UserDefaults when logging in.
    Now all that’s left is to revoke when the user leaves the service.

      func removeAccount() {
        let token = UserDefaults.standard.string(forKey: "refreshToken")
    
        if let token = token {
          
            let url = URL(string: "https://YOUR-URL.cloudfunctions.net/revokeToken?refresh_token=(token)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "https://apple.com")!
                  
            let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
              guard data != nil else { return }
            }
                  
            task.resume()
            
        }
        ...
        //Delete other information from the database...
        FirebaseAuthentication.shared.signOut()
      }
            
    

    If we’ve followed everything up to this point, our app should have been removed from your Settings – Password & Security > Apps Using Apple ID.

    Thank you.

    Login or Signup to reply.
  2. [UPDATE] Resolution being actively worked on: https://github.com/firebase/firebase-ios-sdk/issues/9906#issuecomment-1159535230

    Heads up, a feature request has been created to have Firebase Auth handle the revoking of tokens on user deletion, you can follow it here: https://github.com/firebase/firebase-ios-sdk/issues/9906

    Login or Signup to reply.
  3. I think this should be done from your backend, so as not to expose sensitive data (client_secret) to the application.
    This is how I generate client_secret in .net and call revoke token API endpoint:

    public static class EndUserUtils
    {
    
        //-------------------------- Apple JWT --------------------------
        //Must add System.IdentityModel.Tokens.Jwt from NUGet
    
        using System.Security.Claims;
        using System.Security.Cryptography;
        
        public static string GetAppleJWTToken(IErrorLogService errorLogService)
        {
            var dsa = GetECDsa(errorLogService);
            return dsa != null ? CreateJwt(dsa, "KEY_ID", "TEAM_ID") : null; //Get KEY_ID and TEAM_ID from Apple developer site
        }
    
        private static ECDsa GetECDsa(IErrorLogService errorLogService)
        {
            try
            {
                var keyPath = Path.Combine("..", "Settings", "Keys", "AuthKey_KEY_ID.p8"); //Download from apple developer
                using (TextReader reader = System.IO.File.OpenText(keyPath))
                {
                    var privateKey = reader.ReadToEnd();
                    privateKey = privateKey
                        .Replace("-----BEGIN PRIVATE KEY-----", "")
                        .Replace("-----END PRIVATE KEY-----", "")
                        .Replace("n", "");
                    var ecdsa = ECDsa.Create();
                    ecdsa?.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKey), out _);
                    return ecdsa;
                }
            }
            catch (Exception ex)
            {
                errorLogService?.AddException(ex);
            }
            return null;
        }
    
        private static string CreateJwt(ECDsa key, string keyId, string teamId)
        {
            var securityKey = new ECDsaSecurityKey(key) { KeyId = keyId };
            var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256);
    
            var descriptor = new SecurityTokenDescriptor
            {
                IssuedAt = DateTime.UtcNow,
                Issuer = teamId,
                SigningCredentials = credentials,
                Expires = DateTime.UtcNow.AddMinutes(5), //Define how long generated JWT will be valid
                Audience = "https://appleid.apple.com",
                Subject = new ClaimsIdentity(new[]
                {
                    new Claim("sub", "com.example.appname") //APP_ID 
                })
            };
    
            var handler = new JwtSecurityTokenHandler();
            var encodedToken = handler.CreateEncodedJwt(descriptor);
            return encodedToken;
        }
    }
    

    Calling Apple ‘revoke’ token endpoint from .net core backend

    //Only for SignIn with Apple
    if (!string.IsNullOrEmpty(tokenToRevoke))
    {
        var secret = EndUserUtils.GetAppleJWTToken(_errorLogService);
        if (secret != null)
        {
            var formData = new List<KeyValuePair<string, string>>();
            formData.Add(new KeyValuePair<string, string>("client_id", "com.example.appname"));
            formData.Add(new KeyValuePair<string, string>("client_secret", secret));
            formData.Add(new KeyValuePair<string, string>("token", tokenToRevoke));
            formData.Add(new KeyValuePair<string, string>("token_type_hint", "access_token"));
    
            var request = new HttpRequestMessage(HttpMethod.Post, "https://appleid.apple.com/auth/revoke")
            {
                Content = new FormUrlEncodedContent(formData)
            };
    
            using (var client = _httpClientFactory.CreateClient())
            {
                var result = client.SendAsync(request).GetAwaiter().GetResult();
    
                if (!result.IsSuccessStatusCode)
                {
                    _errorLogService.AddError($"Error revoking Apple idToken: {result.StatusCode}, {result.Content}");
                    
                    //return error to application
                }
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search