`invalid_grant` when exchanging authorization code obtained from `ASAuthorizationAppleIDCredential` on the server-side

Hi,


I'm trying to implement Sign in with Apple where the authorization code is obtained from the

ASAuthorizationAppleIDCredential
native API, then sent to our server for verification via the REST API.


func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {      guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else {           return      }       let authorizationCode = String(data: credential.authorizationCode!, encoding: .utf8) }

This is entirely successful. It uses an App ID with the correct entitlement.


I'm then attempting to exchange this on the server-side:

curl "https://appleid.apple.com/auth/token" \      -d "client_id=***" \      -d "client_secret=***" \      -d "grant_type=authorization_code" \      -d "code=***" \      -d "redirect_uri=***"

The client ID and secret are set up from a Services ID. The client secret JWT is generated correctly (this is validated, because I do not receive an

invalid_client
error).


Unfortunately, this fails with an

invalid_grant
error.


Of course the identifier of the Services ID (and therefore the client ID) and the identifier of the App ID are entirely different. This is because it's not possible to make a Services ID with the same identifier as the App ID. It is unclear what client ID should be used, or how to make a Services ID for use with data from the native API.


The redirect URI parameter is also very unclear - since there is no redirect in the process. However I have found that the result is always the same, regardless of what the parameter is set to (including if it is omitted entirely).


I suspect this process is failing because the authorisation code simply wasn't created for the client ID of the Services ID. However there is no way to indicate the intended use on the native API, as far as I can see.


I have scoured all the documentation at length and have still not gotten anywhere. What is the intended approach to take an authorization code from

ASAuthorizationAppleIDCredential
and exchange it with the REST API?


Thanks!

Accepted Answer

Thanks. That makes sense.


For anyone else having trouble with this, this is how you can generate the client secret JWTs for different client IDs from the same key (Node JS example):


const key = `
-----BEGIN PRIVATE KEY-----
***
-----END PRIVATE KEY-----
`;

const teamId = '***';
const keyId = '***';
const webClientId = 'com.example.backend-auth-system'; // the Services ID
const appClientId = 'com.example.MyApp'; // the App ID

const jsonwebtoken = require('jsonwebtoken');

// for web use
jsonwebtoken.sign({}, key, {
  algorithm: 'ES256',
  expiresIn: '1d',
  audience: 'https://appleid.apple.com',
  subject: webClientId,
  issuer: teamId,
  keyid: keyId,
});

// for native use
jsonwebtoken.sign({}, key, {
  algorithm: 'ES256',
  expiresIn: '1d',
  audience: 'https://appleid.apple.com',
  subject: appClientId,
  issuer: teamId,
  keyid: keyId,
});


Presumably the Services IDs/App IDs all need to be associated with the same primary App ID. The key is then associated to that group via the primary App ID too.

Answers

Unfortunately looks like some of my formatting got mangled.


These were the code snippets provided above:


func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
  guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else {
       return
  }

  let authorizationCode = String(data: credential.authorizationCode!, encoding: .utf8)
}


curl "https://appleid.apple.com/auth/token" \
  -d "client_id=***" \
  -d "client_secret=***" \
  -d "grant_type=authorization_code" \
  -d "code=***" \
  -d "redirect_uri=***"

Hi,


The client_id used when calling the token endpoint should match the native app's app id. The services ID should not be used here and using that would result in failure due to mismatch between the client_id for which the authorization was granted and the one that is presenting the code for validation.


The services ID is useful only for web apps where both the authorization and the grant code validation should use the services Id as the client_id.


Also, redirect_uri is optional and for native apps, since the authorization grant code was not sent to a redirect_uri

Thanks. That makes sense.


For anyone else having trouble with this, this is how you can generate the client secret JWTs for different client IDs from the same key (Node JS example):


const key = `
-----BEGIN PRIVATE KEY-----
***
-----END PRIVATE KEY-----
`;

const teamId = '***';
const keyId = '***';
const webClientId = 'com.example.backend-auth-system'; // the Services ID
const appClientId = 'com.example.MyApp'; // the App ID

const jsonwebtoken = require('jsonwebtoken');

// for web use
jsonwebtoken.sign({}, key, {
  algorithm: 'ES256',
  expiresIn: '1d',
  audience: 'https://appleid.apple.com',
  subject: webClientId,
  issuer: teamId,
  keyid: keyId,
});

// for native use
jsonwebtoken.sign({}, key, {
  algorithm: 'ES256',
  expiresIn: '1d',
  audience: 'https://appleid.apple.com',
  subject: appClientId,
  issuer: teamId,
  keyid: keyId,
});


Presumably the Services IDs/App IDs all need to be associated with the same primary App ID. The key is then associated to that group via the primary App ID too.

Hi billinghamj,


Thanks for providing the file to generate the App & Server Token.


So while validating the authorization code I am sending the following parameters with the associated values:


client_id: "App ID"
client_secret: "App Token"
grant_type: "authorization_code"
code: "sent from the app"


But still I am getting invalid_grant error


@billinghamj so can you please let me know the final curl request & the associated values used by you to validate the authorization_code sent from the App.

I succeed on iOS 13, but I failed on watchOS 6, I wrote watch app's bundle id as client key, and told me "invalid_grant", where's wrong?

Hi billinghamj, thank you for providing the example. Would you mind let us know how you generated the key for signing JSON web token?


Update:

Never mind. For those who have the same question: the private key can be downloaded from the developer portal for only one time. It's a p8 file containing the private key string.

I finally managed to solve it. If anyone is facing the same issue make sure the "client_id" matches the bundle identifer of the sample app that's generating the Authorization Code.

Thats exactly what i was dealing with, thank you so much for sharing! I agree, there is a lot of opportunity for Apple to improve their documentation.

Hi, Lara,


Did you manage to figure out what was going wrong? I'm trying to implement Sign in with Apple at the moment and I'm getting "invalid_grant" when trying to validate the token.

Is there anything else you can think could be going wrong here as I'm getting "invalid_grant" and the client_id I pass when validating is the same bundle id that I'm using in the app. Can't figure out what else could be going wrong here. Thanks.

In iOS I passed the auth code straight to my post, however billinghamj encodes with .utf8 before sending it.


let authorizationCode = String(data: appleIDCredential.authorizationCode!, encoding: .utf8)


Make sure you take off optional words from that as well.


Hope it helps!

I think authorizationCode is valid for 5 minutes, so make sure you are calling Apple's endpoint to verify it within this period.

Hi nik6019,


client_id you mention is services ID created by apple portal. Is that right?

Thank-you for replying back to your own question with how you solved it! Turns out that was my original issue also.


I then ran into the `invalid_grant` issue again a bit later and found that if you don't provide the original `redirect_uri` it will also raise this error. So if you are validating an auth code for anything but native iOS, you need to return the `redirect_uri` that you sent to Apple.

Hi Jeremy,


You can omit the redirect_uri in request to verify the auth code, just simply remove on request.


The parameter

redirect_uri
is only required if the application provided a
redirect_uri
in the initial authorization request when authorizing a user with your app.


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