19

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.

philipxy
  • 14,867
  • 6
  • 39
  • 83
Guilherme
  • 877
  • 9
  • 16
  • `client_id, client_secret` is not something you recover it is your bundle id and the client secret is built and signed. Firebase will likely never provide a method to do this because the signature requires your private key for the secret. https://stackoverflow.com/questions/72476140/how-can-i-find-the-desired-client-id-and-client-secret-values-for-the-appleid-ap/72480819#72480819 – lorem ipsum Jun 02 '22 at 19:23
  • @loremipsum Firebase already do this with push notification. We generate the notification key file, and send to FB, and FB sends the notifications using my key. In my opinion, should be possible to do the same with the logout flow. – Guilherme Jun 05 '22 at 14:42
  • You can submit a feature request. I wouldn’t count on it. This API isn’t new and the requirement has been a long time coming. But who knows… – lorem ipsum Jun 05 '22 at 14:56
  • How could we get the `token`? does it from this API https://appleid.apple.com/auth/token? – zangw Jun 14 '22 at 09:32
  • When user authenticate you get the token. Look the idTokenString on this FB doc https://firebase.google.com/docs/auth/ios/apple – Guilherme Jun 14 '22 at 10:46
  • @Guilherme, thank you very much for your response. One more question, how to validate that the revoke token API is successful? In our test, it seems the revoke token API always returns 200 even with incorrect parameters. For details, please refer to https://stackoverflow.com/questions/72556424/how-to-validate-the-apple-revoke-token-api-appleid-apple-com-auth-revoke-succe?noredirect=1&lq=1 – zangw Jun 15 '22 at 03:10
  • After reading your question I was scared. I read the Apple documentation and saw this text: For either token revocation request, the revoke endpoint returns a 200 response code without a response body after the server invalidates the token value, or if the token value was previously invalidated. If the response contains an error, please see ErrorResponse for the specific error code provided in the response body. Take a look at the response body: https://developer.apple.com/documentation/sign_in_with_apple/errorresponse – Guilherme Jun 15 '22 at 10:26
  • @Guilherme, since the response code is 200, there is no response body. It seems we have no idea how to investigate it... – zangw Jun 15 '22 at 11:07
  • 5
    Please put the solution that you've found in your own answer to the question, rather than adding it to the question. I've rolled back the edit adding the solution to the question. – Ryan M Jun 16 '22 at 00:06
  • @Guilherme, when we try to get the token through `auth/token` API , we got the error `{"error":"invalid_grant","error_description":"The code has expired or has been revoked."}` , unfortunately, we cannot get the useful information from this linked https://stackoverflow.com/questions/59753274/new-apple-sign-in-keeps-throwing-error-http-400-invalid-grant/64114694#64114694 – zangw Jun 16 '22 at 13:27
  • 1
    @zangw I am understanding that you are trying to generate client_secret and not get the user token right?! I download the Authkey(Sign In with Apple) from Apple. There you have the key to generate the client_secret token. I created with Java from this code: https://stackoverflow.com/a/68380631/1966079 There you can see that broken lines were removed and the "header and footer" file. – Guilherme Jun 16 '22 at 14:53
  • 1
    @zangw About the always get 200 as response, I sent wrong data using postman and is true, always get 200. But I sent my app to validation with all this that is described on this post and Apple accept my App and removed the red warn from Ap Connect. I thing that if you generate a right client_secret and send a valid token from user and your valid client_id and everything will be right. – Guilherme Jun 16 '22 at 15:03
  • @RyanM I only wrote what I had difficulty understanding. The documentation is not very clear. If you want to remove the comment feel free. I already solved my problem. – Guilherme Jun 16 '22 at 15:13
  • @Guilherme, thank you very much for your answer, we make the revoke token API successfully through the access token of `token/auth`, for details please refer to https://stackoverflow.com/a/72656409/3011380. – zangw Jun 17 '22 at 08:52

3 Answers3

8

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.

Youngho Joo
  • 159
  • 3
  • Thanks for your answer, client_secret I am creating on my backend server. But I understood your point, which was enlightening to me. – Guilherme Jun 05 '22 at 15:16
  • Does the appleIdToken is same as the identityToken of apple login response? – zangw Jun 14 '22 at 09:30
  • @zangw Yes it is same as the identityToken provided when delegate method get called of Sign In With Apple – Paresh Patel Jun 15 '22 at 05:07
  • 1
    @PareshPatel, we make the revoke token api successfully with access token of `auth/token`, for details please refer to https://stackoverflow.com/a/72656409/3011380 – zangw Jun 17 '22 at 08:50
  • Failed to check if token is correct because API's response is ambiguous. As @zangw said, I think you need to send the correct token. – Youngho Joo Jun 18 '22 at 12:54
  • How do I get `access_token` and `refresh_token` that need to be revoked? – algrid Jun 20 '22 at 16:29
  • @algrid You must obtain a refresh_token with a code valid for 10 minutes at the time of login. We will promptly organize and update the contents. – Youngho Joo Jun 22 '22 at 02:58
  • I updated my answer. After getting the appleIDCredential.authorizationCode from the app, you need to get the refresh token through the auth/token API. – Youngho Joo Jun 22 '22 at 11:17
  • @YounghoJoo I'm not able to get a refresh token using the authorization code? Can you help me? I've posted my question here (https://stackoverflow.com/questions/72797282/how-to-get-access-token-to-revoke-for-existing-sign-in-with-apple-users) – Rahul Vyas Jun 29 '22 at 12:50
  • @YounghoJoo I have read your steps. As exactly I've followed in my case, but with addition of 'access_token'. Can you tell me why you haven't used 'access_token', when Apple documentation says to use it? Does it work properly without 'access_token' ? – Dharmendra Nov 14 '22 at 07:10
  • AuthorizationCode leaves only 5 minutes, and can be used only once. Otherwise, I'm getting 400 error. I haven't found this information anywhere except apple official documentation https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens – Yura Babiy Mar 05 '23 at 21:08
3

[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

Calholl
  • 63
  • 5
  • Officially supported by Firebase is more accurate and convenient than my answer. We hope to get support quickly before the date of Apple's guidance. – Youngho Joo Jun 18 '22 at 09:32
  • @CalHoll has there been any update on when they will implement this? – Dennis Ashford Jul 13 '22 at 12:36
  • They're now saying that it won't be till Q4 as it needs security audit by the team. In the meantime there is a Functions solution that has been recommended here: https://github.com/jooyoungho/apple-token-revoke-in-firebase – Calholl Jul 22 '22 at 03:40
0

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
            }
        }
    }
}
ihmgsm
  • 23
  • 6
  • 1
    You generate the credential on the device itself, so it actually doesn't make sense to perform the revoke on backend in this case. Particularly for Firebase, as it has no idea _how_ you generated credential, only that it's approved by Apple as legitimate. – KLD Jun 18 '22 at 20:07