Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to revoke tokens during account deletion? [Apple Policy deadline June 30 2022] #282

Open
sushrut-desora opened this issue Jun 8, 2022 · 42 comments
Labels
enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed

Comments

@sushrut-desora
Copy link

According to apple docs, apps will need to provide an option to delete user account by June 30, 2022.

When signing-in via apple the app also needs to revoke the user tokens as mentioned in the FAQs on the same page and as documented here.

How can we revoke user token using this library? Does v2.2.1 support revoking user token? Is it integrated in the logout flow or it that a different API method ?

Library version - v2.2.1

@mikehardy mikehardy changed the title How to revoke tokens during account deletion ? How to revoke tokens during account deletion? [Apple Policy deadline June 30 2022] Jun 8, 2022
@mikehardy
Copy link
Collaborator

Hi there! The module does not support it currently

I see two paths to implement this:

  1. Until / unless this module has an implementation, you will need to implement a server feature somewhere that handles it. This is possible, and is completely under your control, but is not a great implementation path in my opinion

  2. implement a PR here follows those docs by using the information we have available within the library (regarding all the app information - client_id / client_secret / current token) to post to the URL in the docs and revoke the refresh and access token

I would love to see a PR implementing option 2 🙏 🙏 and would collaborate on it. I'm not sure if I'll have time prior to then to implement it, or if I do it will be closer to the deadline so may not offer enough time for others to get it in their app version review internally and out the door by June 30.

It's also possible that in review the reviewers may have no way of knowing whether the token deletion requests are happening or not. So you may be able to pass review even without token revocation as long as you call the logout operation here during delete account. I have zero evidence one way or the other how they will enforce this during review, so unless someone else has evidence either way we will have to see if apps are rejected until/unless the token revocation is implemented here

@mikehardy mikehardy added enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed labels Jun 8, 2022
@andrejandre
Copy link

andrejandre commented Jun 21, 2022

A lot of the community is concerned about this upcoming requirement (by community, I mean iOS developers all around - regardless of tech stack).

I would not gamble with the apple review process. The purpose of token revocation is to remove associations to a developer's app from a user's 'Apps using Sign In With Apple' settings.

If the token revocation is successful, you should be able to see that the user's setting no longer holds an association to the app. I suspect that reviewers will be able to do a basic test to verify this. I noticed in App Review that my reviewers were both creating and deleting accounts in my app (even before the requirement has become relevant).

I have tried to implement this natively, but have not had success. Another option, which I have not tried, is to make a call to my backend service (custom function living in Firebase), but that would not be an elegant way of handling this.

Apple has done a poor job at documenting this, other than outlining the requirement itself, and showing some curl HTTP examples.

If you want to be in tune with others struggling with this requirement, including myself, please see my post below. I hope all of us from across different tech stacks can overcome this requirement. I am aware that Firebase is also looking to implement a custom solution to this, but it remains unclear when or how we'll be able to access it, if at all. Fingers crossed.

https://stackoverflow.com/questions/72399534/how-to-make-apple-sign-in-revoke-token-post-request?noredirect=1#comment128385577_72399534

@mikehardy
Copy link
Collaborator

I wouldn't want to personally gamble either, my preference is as stated:

  1. implement a PR here follows those docs by using the information we have available within the library (regarding all the app information - client_id / client_secret / current token) to post to the URL in the docs and revoke the refresh and access token

I would love to see a PR implementing option 2 pray pray and would collaborate on it.

You state:

I am aware that Firebase is also looking to implement a custom solution to this

Looks like this is what you mean? firebase/firebase-ios-sdk#9906 (comment)

@mikehardy
Copy link
Collaborator

This appears to have a working solution for some but requires a fair bit of documentation on how to set up the JWT etc, and an implementation here of the code sketched out in the solution: https://stackoverflow.com/a/72656672/9910298

@algrid
Copy link

algrid commented Jun 21, 2022

@mikehardy The missing part in that solution is how to get access_token and refresh_token that we need to supply in order to revoke them. Does Firebase store them, can we get them somehow?

I suppose that Firebase at some point calls auth/token ( https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens ) in order to generate those tokens, but maybe I'm wrong.

@mikehardy
Copy link
Collaborator

mikehardy commented Jun 21, 2022

Well, Firebase is actually separate from this module, right? I mean obviously I'm firebase-interested, as maintainer over there at react-native-firebase but you may use this module without it. That implies that we should have a way to get the access_token and refresh_token right?

Perhaps via re-authentication here?

This will be called https://developer.apple.com/documentation/authenticationservices/asauthorization?language=objc

here

- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization {

We get an authorization code from that I believe?

Now - with that, I think you can obtain a refresh token if you HTTP POST the authorization code along with app configuration secrets here https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens

Now you've got a refresh, which you may invalidate per token revoke docs associated with this issue / that stackoverflow post

Open question: I guess firebase servers have their own refresh token for the firebase / apple sign in integration, so: when you get a new refresh token associated with a specific user, does that invalidate the old one? And then if you invalidate the new refresh token, are we all clear? (it may be necessary to use the new refresh token to obtain an access token first - it appears based on a quick scan of related libraries that old refresh tokens are revoked by identity providers when new refresh tokens are issued, but frequently only after a grace period or a first use in order to give grace to mobile application environments where network connection failure may mean a refresh token request is issued but connection breaks before new refresh token is received)

If so, we're set. If not, we need firebase to allow us to get the existing refresh token / access token they have, somehow ?

@tmoubarak
Copy link

tmoubarak commented Jun 21, 2022

This is a possible work around, if we follow the same steps it may work out:
https://stackoverflow.com/a/72498906/321506
Will attempt it on my end and let you guys know!

@mikehardy
Copy link
Collaborator

I don't think there needs to be a workaround based on my investigation in comment above.
I think the test is:

  • alter this module to, perhaps, store the authorization code in user keychain (so it's available, though it is only valid for 5 minutes per apple docs). Display it in the UI here along with it's expiration so it's visible for testing.
  • make a test button here called "get refresh token", enabled if authorization code has not expired, and implement it in javascript as a POST to the apple REST API using the authorization code and your app secrets as documented. Store the refresh token in keychain so it's available, display the refresh token here in the app if it's available (so now you can see it changing as you request new ones by hitting the button?)
  • make a test button here called "get access token" and implement it here in javascript as a POST to the apple REST API using the refresh token. Store it in keychain for persistent access. Display the access token so you can see it changing

Now

make a new API in javascript that POSTs to the revoke API with either or both of the access token and refresh token and make a button that invokes it

Then you can test if making new refresh tokens invalidate old refresh tokens, and if revoking the refresh token then removes the app from the accounts token list as visible at "the apple id binding information is deleted under Apps Using Apple ID of Settings" per the stack overflow comment we're all linking to above

If it does, we're literally done here, solution implemented. If not then we know we have a hard block on getting access to whatever the existing refresh / access tokens are for whoever is paired with this library in practice (for example, react-native-firebase / firebase, or flutterfire / firebase etc)

@algrid
Copy link

algrid commented Jun 21, 2022

@mikehardy ouch, sorry, I indeed missed that this repo isn't actually related to Firebase. :)

I'm definitely missing something what happens during Apple Sign In + Firebase Authentication.

It looks like with Firebase we don't use authorizationCode (that we get in the didCompleteWithAuthorization call) at all. Or I simply can't find where it happens.
We use identityToken only, passing it over to Firebase.

So can Firebase generate any refresh tokens without authorizationCode?
Should we in general revoke any tokens if we don't generate any? Generating tokens only to revoke them later seems weird...

@algrid
Copy link

algrid commented Jun 21, 2022

btw, storing authorizationCode for long time wouldn't probably make sense:

The code is single-use only and valid for five minutes.

as mentioned here https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
If that's about the token we're talking about.

@mikehardy
Copy link
Collaborator

I was just talking about storing authorization codes for display / app restart purposes (in case it restarts? like you hot reload the code?) during this type of testing - in other words for near immediate use

I am also a little vague on exactly how firebase-auth gets what it needs. The vaguery is associated with what exactly identityToken is - perhaps it is the refresh token ? or may be used as such?

Generating a token only to revoke it may seem weird but if generating a new refresh token (that you control and may now revoke) has the side effect of revoking all other associated refresh tokens which may not be in your control (because they are off in the firebase cloud or something) then it sure would be a nice side effect, and all the sudden no longer weird, but crucial to get control of the tokens back for full revocation

@mikehardy
Copy link
Collaborator

Indeed in react-native-firebase we send the identity token in to the OAuth provider along with the nonce but that's it.

https://github.com/invertase/react-native-firebase/blob/c0b5e5c078d82e134c538cbec09d97cc7a35d055/packages/auth/ios/RNFBAuth/RNFBAuthModule.m#L966-L969

  } else if ([provider compare:@"apple.com" options:NSCaseInsensitiveSearch] == NSOrderedSame) {
    credential = [FIROAuthProvider credentialWithProviderID:provider
                                                    IDToken:authToken
                                                   rawNonce:authTokenSecret];

what they are doing with it, I'm not 100% sure. It may be that they never actually create Apple refresh/access tokens, it may be that they decode the token in order to validate it, then assuming it is valid they simply trust it as a basis for emitting firebase (not apple) auth tokens. It may be that they are creating an apple refresh token etc via apple REST API though how they would do that with only the identity token and nonce, and not the authorization code I have no idea.

All of this just needs experimentation I guess.

@mikehardy
Copy link
Collaborator

There is an experimental result that the speculated path of "re-authorize user to get authorizationCode + use authorizationCode to get refresh-token + revoke refresh-token" works, with code linked

#282

So what we need now is a PR here, and it seems the sketch above should serve. I have attempted to research it and post ideas so that it was clear what sort of thing we need here and thus allow me to be a good collaborator and merge things but I need to set expectations clearly: I have no time do the code + testing required to get a working solution.

Someone interested in this functionality is going to have to step up and implement it + test it. I will continue to be available for collaboration + merge + release though, you won't be on your own, I just don't have time for the code+test portion.

@cresenciof
Copy link

Server Side

  • Apple authentication certificate (p8)
  • Extract the PEM key from the p8 certificate (some cases, I saw other solutions directly using the p8 certificate)
  • Client Secret - a JWT token signed with the PEM key (Follow the instructions here)

Also you can refer to this guide I followed the instructions to get my client_secret

On my case on our team we are using GraphQL to communicate our App with the Server but the logic is the same if you are using REST, I added 2 queries and 1 mutation:

queries:

query GetAppleAuthClientSecret{
  appleAuthClientSecret {
    clientSecret
  }
}

query AppleAuthRefreshToken($authorizationCode: String!, $clientSecret: String!){
  appleAuthRefreshToken(authorizationCode: $authorizationCode, clientSecret: $clientSecret){
    refreshToken
  }
}

mutation:

mutation RequestAppleLoginRevocation($refreshToken: String!, $clientSecret: String!){
  requestAppleLoginRevocation(clientSecret: $clientSecret, refreshToken: $refreshToken){
    id
    email
  }
}

behind scene

Apple API Calls

Get the Refresh Token

refers to refers to https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens

curl -v POST "https://appleid.apple.com/auth/token" \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'client_id=CLIENT_ID' \
-d 'client_secret=CLIENT_SECRET' \
-d 'code=CODE' \
-d 'grant_type=authorization_code'

Revoke Token

refers to https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens

curl -v POST "https://appleid.apple.com/auth/revoke" \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'client_id=CLIENT_ID' \
-d 'client_secret=CLIENT_SECRET' \
-d 'token=REFRESH_TOKEN' \
-d 'token_type_hint=refresh_token'

Client Side:

I added a option to request the account deletion, when users press the button I call

// call the GetAppleAuthClientSecret query
const clientSecret = ...

appleAuth.performRequest({
          requestedOperation: AppleAuthRequestOperation.LOGOUT,
        })
        .then(response => {
          const authorizationCode = response.authorizationCode;
          return refetchToken({
            authorizationCode: authorizationCode ?? '',
            clientSecret: clientSecret,
          });
        })
        .then(response => {
          const refreshToken =
            response?.data?.appleAuthRefreshToken?.refreshToken;
            
          refreshToken &&
            revokeAccess({
              variables: {
                clientSecret: clientSecret,
                refreshToken: refreshToken,
              },
            });
        });

The appleAuth performed request to LOGOUT users opens a modal (like when we call LOGIN), and it returns the authorization code if is executed successfully. We will use this to get the refresh token (need to mention that this is the token we need to revoke).

The client secret just I said before is the JWT token signed using the Apple Authentication Certificate so, we can generate it without any parameter.

So:

  • client secret - call GetAppleAuthClientSecret
  • authorization code - call appleAuth LOGOUT (with this library)

once we have these parameters we can call the AppleAuthRefreshToken query to get our refresh_token
and finally call the RequestAppleLoginRevocation mutation using the client_secret and the refresh_token

Also we can use the listener provided by this library onCredentialRevoked to verify that it is working or by going to Settings -> Apple ID -> Password and Security -> Apps using Sign In With Apple

@mikehardy
Copy link
Collaborator

Okay, so this sounds like it's an "understood" problem technically, but we still need a PR here that will implement it given the correct configuration (certs and JWTs etc)? Or @cresenciof are you implying that there is no way to do this in this module / on device and it requires a server running? I was under the impression we could do this in the app if we had the right things configured and called the right REST APIs ?

@cresenciof
Copy link

@mikehardy You're right, we can call this REST API's directly from the client. The only thing required is the client_secret, I have read that the client_secret can be generated with an expiration time of up to 6 months.
I don't consider generating the JWT from the client, I think it's not possible and we shouldn't expose a private key. So yes, we also need a server side implementation to at least generate the client secret

It gives us two ways to work around it:

  • Get a client secret from a API on each request (Our own server o cloud function)
  • Set a client_secret env with 6 months of validity(involves doing some updates periodically)

@mikehardy
Copy link
Collaborator

Set a client_secret env with 6 months of validity(involves doing some updates periodically)

From the perspective of a developer where cloud functions cost $ and set up time + reliability concerns + it's own updates etc but an update every 6 months is almost free, this seems like a reasonable solution and would work well

Might even be possible (as an enhancement, once it was working) to do console.warn when the secret was approaching expiration etc.

Assuming that works I think it would be a fantastic solution, all self-contained here in the module after initial configuration. Given a deadline of just a few days - even if it is not fantastic in everyone's opinion - it would at least provably work and not add any external server requirements for people

@algrid
Copy link

algrid commented Jun 23, 2022

Using AppleAuthRequestOperation.LOGOUT as suggested by @cresenciof looks interesting. Is it better than getting a refresh token right after sign in and storing it?

@algrid
Copy link

algrid commented Jun 23, 2022

Also, regarding exposing client secret to client side. Am I right that that jwt isn't issued strictly to a user you're authenticating via Apple Sign In? In theory an attacker can take hold of it and somehow use it for some operations on behalf of other users (?) I don't know too much about the details here, but my intuition is that exposing it should be avoided if possible (and it's possible in our case as far as I can see).

@mikehardy
Copy link
Collaborator

@algrid I think removing the need to store a refresh token is a positive, and it appears that LOGOUT will prompt a user interaction the same as LOGIN so the user experience has the same number of interactions. On balance then this looks better than storing a token

As for exposing the JWT representing the client secret, I think this would potentially let an attacker perform "sign in with apple" API calls as your Apple Developer account / app combination. The current set of those is sign in / sign out / revoke-token I think. These will prompt user interaction for sign in / sign out at least but it may be possible to spoof yes. As with all things related to security: think very critically about what you are protecting (value of successful attack) what it costs to defeat any protection (cost of attack) and act according to your tradeoffs. I think the cost of attack is reasonably low here as an app can be decompiled, you have to assume the JWT is recoverable+recovered. What is the value - hard to say. Someone is now associated (or disassociated?) with your app (not even the spoofiing app?). I'm not sure that has value to anyone? I always assume I'm missing something when I analyze security cost/benefit though so I'll happily learn something if I'm wrong.

@algrid
Copy link

algrid commented Jun 24, 2022

@mikehardy wouldn't it be the most frequent use case when a user is already signed in at the point when account deletion is triggered? At least that's true in my case. I show a 'delete account' button only when I have a user, otherwise it doesn't make sense. So, having to authenticate for logout requires more actions from the user.
From the implementation standpoint I like the idea of not having to store the refresh token, but requiring authentication for a users who's already authenticated looks annoying.

@mikehardy
Copy link
Collaborator

@algrid I agree with you. I also only show delete on a screen that is past my login gate.

At the same time, I'm unaware of any way to get authorization code (which you can then escalate to refresh token) without triggering some authentication interaction with the user. I would definitely de-compose the activities in any PR here (or local work) such that one chunk was

a) "do we have a refresh token? if not let us get one via login (or logout) to get authorization code then use that to get refresh token"

...and then

b) "okay let us use that refresh token plus all our other magical config like JWT etc to revoke tokens"

And the "magical config" part could be a further step where for those comfortable with the risk they could just config the JWT in the app (exposing themselves to spoofed auths if I understand) or it could be an API fetch to a server that generates them with short expiry

@ahmadAlMezaal
Copy link

Server Side

  • Apple authentication certificate (p8)
  • Extract the PEM key from the p8 certificate (some cases, I saw other solutions directly using the p8 certificate)
  • Client Secret - a JWT token signed with the PEM key (Follow the instructions here)

Also you can refer to this guide I followed the instructions to get my client_secret

On my case on our team we are using GraphQL to communicate our App with the Server but the logic is the same if you are using REST, I added 2 queries and 1 mutation:

queries:

query GetAppleAuthClientSecret{
  appleAuthClientSecret {
    clientSecret
  }
}

query AppleAuthRefreshToken($authorizationCode: String!, $clientSecret: String!){
  appleAuthRefreshToken(authorizationCode: $authorizationCode, clientSecret: $clientSecret){
    refreshToken
  }
}

mutation:

mutation RequestAppleLoginRevocation($refreshToken: String!, $clientSecret: String!){
  requestAppleLoginRevocation(clientSecret: $clientSecret, refreshToken: $refreshToken){
    id
    email
  }
}

behind scene

Apple API Calls

Get the Refresh Token

refers to refers to https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens

curl -v POST "https://appleid.apple.com/auth/token" \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'client_id=CLIENT_ID' \
-d 'client_secret=CLIENT_SECRET' \
-d 'code=CODE' \
-d 'grant_type=authorization_code'

Revoke Token

refers to https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens

curl -v POST "https://appleid.apple.com/auth/revoke" \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'client_id=CLIENT_ID' \
-d 'client_secret=CLIENT_SECRET' \
-d 'token=REFRESH_TOKEN' \
-d 'token_type_hint=refresh_token'

Client Side:

I added a option to request the account deletion, when users press the button I call

// call the GetAppleAuthClientSecret query
const clientSecret = ...

appleAuth.performRequest({
          requestedOperation: AppleAuthRequestOperation.LOGOUT,
        })
        .then(response => {
          const authorizationCode = response.authorizationCode;
          return refetchToken({
            authorizationCode: authorizationCode ?? '',
            clientSecret: clientSecret,
          });
        })
        .then(response => {
          const refreshToken =
            response?.data?.appleAuthRefreshToken?.refreshToken;
            
          refreshToken &&
            revokeAccess({
              variables: {
                clientSecret: clientSecret,
                refreshToken: refreshToken,
              },
            });
        });

The appleAuth performed request to LOGOUT users opens a modal (like when we call LOGIN), and it returns the authorization code if is executed successfully. We will use this to get the refresh token (need to mention that this is the token we need to revoke).

The client secret just I said before is the JWT token signed using the Apple Authentication Certificate so, we can generate it without any parameter.

So:

  • client secret - call GetAppleAuthClientSecret
  • authorization code - call appleAuth LOGOUT (with this library)

once we have these parameters we can call the AppleAuthRefreshToken query to get our refresh_token and finally call the RequestAppleLoginRevocation mutation using the client_secret and the refresh_token

Also we can use the listener provided by this library onCredentialRevoked to verify that it is working or by going to Settings -> Apple ID -> Password and Security -> Apps using Sign In With Apple

Thank you for dropping this solution @cresenciof, we tried to do the same but we are getting an invalid_client error response on the generate auth token endpoint, regardless of the error(even if you drop an empty body you'll get the same), although we followed this documentation to generate the client_secret and still not working.

Any suggestions?

@cresenciof
Copy link

@AhmadMazaal try first to use Postman or another REST client to rule out some problem related to the CORS client/server configuration, I received this error some other time but in my case it was sending an incorrect grant_type, also the Content-Type: application/x-www-form-urlencoded is a very important step to take into account.

@algrid
Copy link

algrid commented Jun 27, 2022

@AhmadMazaal I would also re-check your jwt fields and that you're using correct p8 key and correct signing algorithm.

I implemented a similar flow (my GCFs are in Python) and it works.

@algrid
Copy link

algrid commented Jun 27, 2022

@mikehardy This argument about account deletion being a 'sensitive' operation actually makes sense: firebase/firebase-ios-sdk#9906 (comment)

So yeah, I think re-authenticating user for that is the best way of doing it. And it's convenient to not have to store a refresh token. :)

@ahmadAlMezaal
Copy link

ahmadAlMezaal commented Jun 28, 2022

@AhmadMazaal try first to use Postman or another REST client to rule out some problem related to the CORS client/server configuration, I received this error some other time but in my case it was sending an incorrect grant_type, also the Content-Type: application/x-www-form-urlencoded is a very important step to take into account.

Thank you for this suggestion, I tried the same request for both endpoints and it worked pretty well on Postman.

The problem was with us using Axios, it was always serializing the body to multipart/form-data instead of application/x-www-form-urlencoded, although it was included in the header.

Found the solution in this stackoverflow question.

It should look something like this

const config =
 {
       headers: {
             'Content-Type': 'application/x-www-form-urlencoded'
        }
 };
   

  const authTokenBody = new URLSearchParams(
           {
                  client_id: 'com.example.ex',
                  client_secret: CLIENT_SECRET,
                  code: authorizationCode,
                  grant_type: 'authorization_code'
           }
  );

   const generateAuthTokenUrl = 'https://appleid.apple.com/auth/token';
   const authTokenResponse = await axios.post(generateAuthTokenUrl, authTokenBody, config);


   const revokeAuthTokenBody = new URLSearchParams(
        {
           client_id: 'com.example.ex',
           client_secret: CLIENT_SECRET,
           token: authTokenResponse.data.refresh_token,
           token_type_hint: 'refresh_token'
        }
    );
    
     const revokeAuthTokenUrl = 'https://appleid.apple.com/auth/revoke';
  
     const revokeAuthTokenResponse = await axios.post(revokeAuthTokenUrl, revokeAuthTokenBody, config);

Also found this helpful tutorial from MongoDB to generate the CLIENT_SECRET

Hope it helps anyone struggling with the same

@ahmadAlMezaal
Copy link

@cresenciof After doing all the above successfully, the app is still saved in the sign in settings of Apple and was not removed, we are using our backend and Firebase to save user data, is that related?

@mikehardy
Copy link
Collaborator

@AhmadMazaal it sounds like the token revocation has not gone exactly as planned. Note that saving data in any persistent location related to the user is orthogonal that is, it is a separate-but-related issue. You are responsible for deleting any related user data per Apple requirements (stated differently: they have certain categories they allow you to maintain such as data you must retain for legal reasons - you are subject to their requirements and should be familiar with them and should delete all related data per their requirements)

@ahmadAlMezaal
Copy link

ahmadAlMezaal commented Jun 28, 2022

@AhmadMazaal it sounds like the token revocation has not gone exactly as planned. Note that saving data in any persistent location related to the user is orthogonal that is, it is a separate-but-related issue. You are responsible for deleting any related user data per Apple requirements (stated differently: they have certain categories they allow you to maintain such as data you must retain for legal reasons - you are subject to their requirements and should be familiar with them and should delete all related data per their requirements)

@mikehardy Indeed you are right, thank you for the reply.

It appears that we had a typo in the token property of the revokeAuthTokenBody body. I will edit the previous comment match the working solution

@Romick2005
Copy link

Can anyone confirm that apple token revoke also remove app from apple allowed to signIn list?
Apple Settings shows which apps you are currently using with Sign in with Apple.
It is located in Settings -> Password & Security -> Apps Using Apple ID
removing-app-from-settings-sign-in-with-apple-revoke-api-for-account-deletion

@mikehardy
Copy link
Collaborator

@Romick2005 yes, it's confirmed several times in the related firebase issue, firebase/firebase-ios-sdk#9906 (comment)

It appears now that people are able to pass App Store Review with the cloud-function-based solution linked/recommended in that solution and now it's down to some small items like "if the user deletes account and we revoke token, now we don't seem to get name + email if they register again post-delete", which is perhaps a separate issue

@trickeyd
Copy link

trickeyd commented Jul 5, 2022

Hello - sorry to pester in multiple threads! I'm trying to understand how to make this work for our app:
firebase/firebase-ios-sdk#9906 (comment)

I'm planning to reauthenticate with this method at the point the user attempts deletion. I guess as it stands I can't use this lib for any of this process now that the key is required?

How is it that this lib authenticates without a key anyway out of interest?

Thanks

@mikehardy
Copy link
Collaborator

Hey there - yeah I saw your other post on firebase-ios-sdk, this stuff is tricky yes. If by key you mean the p8 that's supposed to be generated/downloaded from Apple developer console for the app, then uploaded to firebase project config for apple auth, if you don't have that I am also confused. I thought that was a fundamental requirement. As such, if it is working without that I'm not sure how? And I'm not sure how you can use the sign in with apple REST API without it as I believe it is required to generate the JWT. Please note two things though: 1) I have not had time to implement this myself so I've been active here but just listening+thinking and that's no substitute for actually doing it myself so I know, and 2) I'm traveling now so apart from this comment I won't have time myself to actually do it for my apps yet either so I'm not the most useful at the moment, apologies

@nachoSource
Copy link

nachoSource commented Jul 12, 2022

Hello!

@AhmadMazaal try first to use Postman or another REST client to rule out some problem related to the CORS client/server configuration, I received this error some other time but in my case it was sending an incorrect grant_type, also the Content-Type: application/x-www-form-urlencoded is a very important step to take into account.

Thank you for this suggestion, I tried the same request for both endpoints and it worked pretty well on Postman.

The problem was with us using Axios, it was always serializing the body to multipart/form-data instead of application/x-www-form-urlencoded, although it was included in the header.

Found the solution in this stackoverflow question.

It should look something like this

const config =
 {
       headers: {
             'Content-Type': 'application/x-www-form-urlencoded'
        }
 };
   

  const authTokenBody = new URLSearchParams(
           {
                  client_id: 'com.example.ex',
                  client_secret: CLIENT_SECRET,
                  code: authorizationCode,
                  grant_type: 'authorization_code'
           }
  );

   const generateAuthTokenUrl = 'https://appleid.apple.com/auth/token';
   const authTokenResponse = await axios.post(generateAuthTokenUrl, authTokenBody, config);


   const revokeAuthTokenBody = new URLSearchParams(
        {
           client_id: 'com.example.ex',
           client_secret: CLIENT_SECRET,
           token: authTokenResponse.data.refresh_token,
           token_type_hint: 'refresh_token'
        }
    );
    
     const revokeAuthTokenUrl = 'https://appleid.apple.com/auth/revoke';
  
     const revokeAuthTokenResponse = await axios.post(revokeAuthTokenUrl, revokeAuthTokenBody, config);

Also found this helpful tutorial from MongoDB to generate the CLIENT_SECRET

Hope it helps anyone struggling with the same

Here we have another way to generate the client_secret JWT. When doing this, always remember to use your last 'kid' as Apple seems to allow the use of only one of them.

The keys' location can be found here .
Hope this will be useful as it was for me too!

@swikars1
Copy link

I did this in my project which is using express backend and this npm package in react native.

  • need to generate sign in with apple key and download it(p8 file)
  • put your p8 file content on env variable
  • need to expose this api from backend , I'm using jsonwebtoken module here.

Setting env:
APPLE_CLIENT_SECRET_P8="-----BEGIN PRIVATE KEY-----abcsdsd\n123\nsomerandomkey-----END PRIVATE KEY-----"

I'm using it like in ts file:

function loadFromEnv(key) {
    if (typeof process.env[key] !== 'undefined') {
        return process.env[key]
 }
 throw new Error(`process.env doesn't have the key ${key}`)
}

config: {
    // other config properties
    clientSecretP8: loadFromEnv('APPLE_CLIENT_SECRET_P8')?.replace(/\\n/g,'\n'),
}

Here I'm signing the private p8 key. You need to expose this from a route.

import * as jwt from 'jsonwebtoken'

try {
   const clientSecretJwt = jwt.sign(
      {
         iss: 'YOUR_APPLE_ISSUER_ID',
         iat: Math.floor(Date.now() / 1000),
         exp: Math.floor(Date.now() / 1000) + 12000,
         aud: 'https://appleid.apple.com',
         sub: 'com.example.app',
      },
      config.apple.clientSecretP8,
      {
         algorithm: 'ES256',
         header: {
            alg: 'ES256',
            kid: 'ABCDEFG1', // Key ID from apple sign in key
         },
      },
   )
   res.apiSuccess({
      message: 'Client secret for apple generated',
      data: clientSecretJwt,
   })
} catch (error) {
   return res.apiFail({
      message: 'Failed generating token for revoking apple token',
      error,
   })
}

I used api call to get appleClientSecret from above controller.
Passing the acquired appleClientSecret in apple revoke function.

const revokeAppleToken = async (appleClientSecret: string) => {
  try {
    const appleAuthRequestResponse = await appleAuth.performRequest({
      requestedOperation: appleAuth.Operation.LOGIN,
      requestedScopes: [appleAuth.Scope.EMAIL, appleAuth.Scope.FULL_NAME],
    });
    const {authorizationCode} = appleAuthRequestResponse;
    if (!authorizationCode) {
      console.log('Authorization code not found after signin');
    }
    const config = {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    };
    try {
      const authTokenBody = new URLSearchParams({
        client_id: 'com.example.app',
        client_secret: appleClientSecret,
        code: authorizationCode as string,
        grant_type: 'authorization_code',
      });
      const generateAuthTokenUrl = 'https://appleid.apple.com/auth/token';
      const authTokenResponse = await axios.post(
        generateAuthTokenUrl,
        authTokenBody,
        config,
      );
      if (!authTokenResponse.data.refresh_token) {
        console.log('No refresh token data');
      }
      const revokeTokenBody = new URLSearchParams({
        client_id: 'com.example.app',
        client_secret: appleClientSecret,
        token: authTokenResponse.data.refresh_token as string,
        token_type_hint: 'refresh_token',
      });
      const revokeAuthTokenUrl = 'https://appleid.apple.com/auth/revoke';
      await axios.post(revokeAuthTokenUrl, revokeTokenBody, config);
    } catch (e) {
      console.error(e);
    }
  } catch (e: any) {
    console.error(e);
  }
}

@akshgods
Copy link

const revokeTokenBody = new URLSearchParams({
client_id: 'com.example.app',
client_secret: appleClientSecret,
token: authTokenResponse.data.refresh_token as string,
token_type_hint: 'refresh_token',
});
const revokeAuthTokenUrl = 'https://appleid.apple.com/auth/revoke';
await axios.post(revokeAuthTokenUrl, revokeTokenBody, config);

so if we implement this, are you able to get email and full name when user login to try again?

@prox2
Copy link

prox2 commented Jun 16, 2023

I did this in my project which is using express backend and this npm package in react native.

  • need to generate sign in with apple key and download it(p8 file)
  • put your p8 file content on env variable
  • need to expose this api from backend , I'm using jsonwebtoken module here.

Setting env: APPLE_CLIENT_SECRET_P8="-----BEGIN PRIVATE KEY-----abcsdsd\n123\nsomerandomkey-----END PRIVATE KEY-----"

I'm using it like in ts file:

function loadFromEnv(key) {
    if (typeof process.env[key] !== 'undefined') {
        return process.env[key]
 }
 throw new Error(`process.env doesn't have the key ${key}`)
}

config: {
    // other config properties
    clientSecretP8: loadFromEnv('APPLE_CLIENT_SECRET_P8')?.replace(/\\n/g,'\n'),
}

Here I'm signing the private p8 key. You need to expose this from a route.

import * as jwt from 'jsonwebtoken'

try {
   const clientSecretJwt = jwt.sign(
      {
         iss: 'YOUR_APPLE_ISSUER_ID',
         iat: Math.floor(Date.now() / 1000),
         exp: Math.floor(Date.now() / 1000) + 12000,
         aud: 'https://appleid.apple.com',
         sub: 'com.example.app',
      },
      config.apple.clientSecretP8,
      {
         algorithm: 'ES256',
         header: {
            alg: 'ES256',
            kid: 'ABCDEFG1', // Key ID from apple sign in key
         },
      },
   )
   res.apiSuccess({
      message: 'Client secret for apple generated',
      data: clientSecretJwt,
   })
} catch (error) {
   return res.apiFail({
      message: 'Failed generating token for revoking apple token',
      error,
   })
}

I used api call to get appleClientSecret from above controller. Passing the acquired appleClientSecret in apple revoke function.

const revokeAppleToken = async (appleClientSecret: string) => {
  try {
    const appleAuthRequestResponse = await appleAuth.performRequest({
      requestedOperation: appleAuth.Operation.LOGIN,
      requestedScopes: [appleAuth.Scope.EMAIL, appleAuth.Scope.FULL_NAME],
    });
    const {authorizationCode} = appleAuthRequestResponse;
    if (!authorizationCode) {
      console.log('Authorization code not found after signin');
    }
    const config = {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    };
    try {
      const authTokenBody = new URLSearchParams({
        client_id: 'com.example.app',
        client_secret: appleClientSecret,
        code: authorizationCode as string,
        grant_type: 'authorization_code',
      });
      const generateAuthTokenUrl = 'https://appleid.apple.com/auth/token';
      const authTokenResponse = await axios.post(
        generateAuthTokenUrl,
        authTokenBody,
        config,
      );
      if (!authTokenResponse.data.refresh_token) {
        console.log('No refresh token data');
      }
      const revokeTokenBody = new URLSearchParams({
        client_id: 'com.example.app',
        client_secret: appleClientSecret,
        token: authTokenResponse.data.refresh_token as string,
        token_type_hint: 'refresh_token',
      });
      const revokeAuthTokenUrl = 'https://appleid.apple.com/auth/revoke';
      await axios.post(revokeAuthTokenUrl, revokeTokenBody, config);
    } catch (e) {
      console.error(e);
    }
  } catch (e: any) {
    console.error(e);
  }
}

for those who are getting error code "invalid_client" with axios make sure to not use new URLSearchParams

@ammaarkhan
Copy link

ammaarkhan commented Sep 11, 2023

Are these implementations no longer needed? Has it been resolved with the code mentioned in the RN Firebase docs (attached below)?

import auth from '@react-native-firebase/auth';
import { appleAuth } from '@invertase/react-native-apple-authentication';

async function revokeSignInWithAppleToken() {
  // Get an authorizationCode from Apple
  const { authorizationCode } = await appleAuth.performRequest({
    requestedOperation: appleAuth.Operation.REFRESH,
  });

  // Ensure Apple returned an authorizationCode
  if (!authorizationCode) {
    throw new Error('Apple Revocation failed - no authorizationCode returned');
  }

  // Revoke the token
  return auth().revokeToken(authorizationCode);
}

11 September Edit: revokeToken is not a function available in the library. I'm not sure why it is being mentioned in the docs, and used in the sample code?

@ammaarkhan
Copy link

For those of you that face this issue again in the future and are using firebase for apple sign in, this might help you: invertase/react-native-firebase#7239. Follow the Test Plan mentioned, and make sure the library version for react-native-firebase/auth is 18.3.0^.

@kockar96
Copy link

kockar96 commented Oct 5, 2023

@mikehardy
Copy link
Collaborator

This looks like something that could do with a documentation patch PR proposal here for the case of using firebase auth and the case of not using firebase auth. Anyone that could post a PR for either or both would be my hero

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests