简体   繁体   中英

New Apple Sign in keeps throwing Error HTTP 400 Invalid_grant

According to apple doc to validate the auth code against Apple we need to POST to http://appleid.apple.com/auth/token with this parameters:

#!java

token = generateJWT(keyId, teamId, clientId, certificatePath);

HttpResponse<String> response = Unirest.post(authUrl)
     .header("Content-Type", "application/x-www-form-urlencoded")
     .field("client_id", clientId)
     .field("client_secret", token)
     .field("grant_type", "authorization_code")
     .field("code", authorizationCode)
     .asString();

where:

  • authorization_code : Is the auth code provided by app client.

  • clientId : is provided by Apple

  • token : is Client Secret. A JWT generate with this code:

#!java

private static String generateJWT(String keyId, String teamId, String clientId, String certificatePath) throws Exception {
        if (pKey == null) {
            pKey = getPrivateKey(certificatePath);
        }

        return Jwts.builder()
                .setHeaderParam(JwsHeader.KEY_ID, keyId)
                .setIssuer(teamId)
                .setAudience("https://appleid.apple.com")
                .setSubject(clientId)
                .setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5)))
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .signWith(pKey, SignatureAlgorithm.ES256)
                .compact();
    }

private static PrivateKey getPrivateKey(String certificatePath) throws Exception {
        //read your key
        try (PEMParser pemParser = new PEMParser(new FileReader(certificatePath))) {
            final JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
            final PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject();
            final PrivateKey pKey = converter.getPrivateKey(object);
            return pKey;
        }
    }

We check that, the JWT, contains all the fields needed by apple:

#!json

{
  "alg": "ES256",
  "typ": "JWT",
  "kid": "6876D87D6"
}
{
  "iat": 1578654031,
  "exp": 1578740431,
  "aud": "https://appleid.apple.com",
  "iss": "57675576576",
  "sub": "com.blahblah.client"
}

But this is the problem. It always return a 400 HTTP Error with this body:

#!json

{"error":"invalid_grant"}

From here we are completely lost. We do not understand why the code is not correct or why it has an invalid_grant error.

In my case, front-end sent identityToken by mistake, instead of authorizationCode . That caused b'{"error":"invalid_grant"}'

I was only able to get the invalid_grant error when sending requests with incorrect authorizationCode to https://appleid.apple.com/auth/token . Make sure you get the code the same way as in the swift code sample below.

I think this error is not about the client_secret . When I change the client_secret from a valid value to an empty string or some incorrect value, I just get the invalid_client error. And when both the authorizationCode and the client_secret are wrong, I also get the invalid_client error.

This is what is needed for the request:

  1. app_id is the bundle identifier for the app
  2. client_secret is the jwt token you create using the information shown in the question and sign with your private key you download from developer.apple.com also see my answer here

  3. grant_type is just the string "authorization_code"

  4. code is the authorizationCode you get from inside the app on a string format, not data format
data = {
    'client_id': app_id,
    'client_secret': client_secret,
    'grant_type': grant_type,
    'code': authorizationCode,
}

headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
}

//python example request

response = requests.request(
    method='POST',
    url='https://appleid.apple.com/auth/token',
    data=data,
    headers=headers,
)

if response.status_code == 200:
    print("200")
else:
    print("error")
print(response.text)

The following swift code can be used to get the authorizationCode , identityToken and userIdentifier on the correct format.

func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {

         let userIdentifier = appleIDCredential.user
         print("userIdentifier: \(userIdentifier)")

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

         let identityToken = String(data: appleIDCredential.identityToken!, encoding: .utf8)!
         print("identityToken: \(identityToken)")

    }
}

We had also faced a similar problem when we were integrating Sign in with Apple in our iOS app using Django backend. We were getting the below error:

<Response [400]> {'error': 'invalid_grant'}

Our problem was we were decoding the authorizationCode as .utf8 from the app before sending it to the backend. Changed it from .utf8 to Unicode.ASCII solved our problem.

func authorizationController(controller: ASAuthorizationController,
                             didCompleteWithAuthorization authorization: ASAuthorization) {
    let credential = authorization.credential as? ASAuthorizationAppleIDCredential
    guard let authorizationCode = credential?.authorizationCode else {
        return
    }
    let authCode = String(decoding: authorizationCode, as: Unicode.ASCII.self)
    print(authCode)
}

After this change we got proper response from Apple:

<Response [200]> {'access_token': 'a80578f5ad9xxx.x.xxx.xxxx'

I am posting here to let everyone know about something that took me 6 hours to figure out myself and apparently isn't documented anywhere. If you're trying to implement Sign in with Apple only on one platform (EITHER ios OR web), you're good. But if you want it to work for both, you'll need two different secrets, and that much is clear.

What was not clear at all is that, if you attempt to validate one code with the wrong credentials (eg you first try the iOS credentials, then follow up with the web ones if it fails) the second, correct request will fail no matter what.

As such, you need to distinguish wether one is connecting from iOS or Android before sending the validation request to Apple, you can't (as is with other logins) "just try" one first and the other if the first failed.

I hit the same problem, the problem is about the algorithm to generate the secret_token.

  • team_id is AppID
  • client_id is bundle ID
  • key_id is KeyID
  • key_file is the path to .p8 file

This is the sample code for generating the secret_token.

NOTED : Seem the authorizationCode expirations is 5 minutes, so please make sure the authorizationCode is valid before making the request.

require 'jwt'

# Update these values with your app's information
team_id = '' #AppID
client_id = '' #bundle ID
key_id = '' #KeyID
key_file = './AuthKey_XXXX.p8'

# Define the JWT's headers and claims
headers = {
  # The token must be signed with your key
  'kid' => key_id,
  'alg' => 'ES256'
}
claims = {
  # The token is issued by your Apple team
  'iss' => team_id,
  # The token applies to Apple ID authentication
  'aud' => 'https://appleid.apple.com',
  # The token is scoped to your application
  'sub' => client_id,
  # The token is valid immediately
  'iat' => Time.now.to_i,
  # The token expires in 6 months (maximum allowed)
  'exp' => Time.now.to_i + 86400*180,
}

# Read in the key and generate the JWT
ecdsa_key = OpenSSL::PKey::EC.new IO.read key_file
token = JWT.encode claims, ecdsa_key, 'ES256', headers

# Print the JWT to stdout
puts token

I had the same issue. The problem was that in the redirect_uri parameter. I only sent the domain name without the complete path.

I sent:

https://myurl.com

I had configured

https://myurl.com/auth/external/apple

I sent the full URL and it worked.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM