invertase / react-native-apple-authentication

A React Native library providing support for Apple Authentication on iOS and Android.
Other
1.41k stars 224 forks source link

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

Open sushrut-desora opened 2 years ago

sushrut-desora commented 2 years ago

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 commented 2 years ago

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 :pray: :pray: 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

andrejandre commented 2 years ago

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 commented 2 years ago

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? https://github.com/firebase/firebase-ios-sdk/issues/9906#issuecomment-1159535230

mikehardy commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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 https://github.com/invertase/react-native-apple-authentication/blob/9040e0e29508c5e7ed444af3b1aee38217a49811/ios/RNAppleAuthentication/RNAppleAuthASAuthorizationDelegates.m#L43

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 commented 2 years ago

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 commented 2 years ago

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

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 commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

https://github.com/invertase/react-native-apple-authentication/issues/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 commented 2 years ago

Server Side

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:

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 commented 2 years ago

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 commented 2 years ago

@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:

mikehardy commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

@mikehardy This argument about account deletion being a 'sensitive' operation actually makes sense: https://github.com/firebase/firebase-ios-sdk/issues/9906#issuecomment-1167301269

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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

@Romick2005 yes, it's confirmed several times in the related firebase issue, https://github.com/firebase/firebase-ios-sdk/issues/9906#issuecomment-1172157915

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 commented 2 years ago

Hello - sorry to pester in multiple threads! I'm trying to understand how to make this work for our app: https://github.com/firebase/firebase-ios-sdk/issues/9906#issuecomment-1168577864

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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

kockar96 commented 1 year ago

Follow this: https://medium.com/swlh/sign-in-with-apple-for-web-using-firebase-auth-js-b13eb7a0e104

mikehardy commented 1 year ago

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