Apple Sign In - invalid_client

Hello,

I am working on apple sign in verification process. Currently, I have a React Native iOS app that uses the @invertase/react-native-apple-authentication package to handle verification on the frontend. This seems to be working just fine.

My iOS app Bundle Id is "com.appname.appname"

When the user signs in, I get an IdentityToken and a AuthorizationCode. I pass both of these values to the backend.

The backend is a .NET Core API. In the Apple Developer Portal, I created 1 key with the enabled service, "Sign In with Apple" and the Primary App Id points to my 1 app. Under Grouped App IDs, it points to my service, "com.appname.appnameservice".

I downloaded the .p8 file from here and saved it for later. My KeyId is "T5LGCK354D".

Then, in Identifiers, I created 2. 1 App ID with the identifier, "com.appname.appname". That also has "Sign In with Apple" and is linked to my primary app id.

The other Identifier is a ServiceId and its Identifier name is "com.appname.appnameservice". It too has "Sign In with Apple" configured, pointed to my Primary app (com.appname.appname) and has 2 domains configured, and 1 return url configured.

It is worth noting, that I also configured Sign in with Apple for Email Communications, and my domain has a green check with SPF next to it.

Finally, in the backend, I have tried a bunch of things. Currently, my api has the following to generate the client secret:

public string GenerateAppleClientSecret()
{
    string privateKey = "MIGTAgEAMBMGByqGSM49..............5rn4GrzFepyloJrr6ECn.....gYIKoZIzj0DAQehR......UZOi88Qdb8ZTU9zM4/jzt0pHZ9uU2HyAbK2//UA6.....mGqkKDqybf";
    string keyId = "T5LGCK354D"; //The 10-character key identifier from the portal.
    string clientId = "com.appname.appnameservice";
    string teamId = "SLT8SJ897V";

    JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();

    var cngKey = CngKey.Import(Convert.FromBase64String(privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);

    var now = DateTime.UtcNow;
    var handler = new JwtSecurityTokenHandler();
    var token = handler.CreateJwtSecurityToken(
        issuer: teamId,
        audience: "https://appleid.apple.com",
        subject: new ClaimsIdentity(new List<Claim> {new Claim("sub", clientId)}),
        expires: DateTime.UtcNow.AddMinutes(5), // expiry can be a maximum of 6 months
        issuedAt: DateTime.UtcNow,
        notBefore: DateTime.UtcNow,
        signingCredentials: new SigningCredentials(new ECDsaSecurityKey(new ECDsaCng(cngKey)), SecurityAlgorithms.EcdsaSha256)
    );
    token.Header.Add("kid", keyId);
    return handler.WriteToken(token);
}

I have tried this too

public string GenerateAppleClientSecret()
{
    string privateKey = "MIGTAgEAMBMGByqGSM49..............5rn4GrzFepyloJrr6ECn.....gYIKoZIzj0DAQehR......UZOi88Qdb8ZTU9zM4/jzt0pHZ9uU2HyAbK2//UA6.....mGqkKDqybf";
    string keyId = "T5LGCK354D";
    string clientId = "com.appname.appnameservice";
    string teamId = "SLT8SJ897V";

    JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();

    //Import the key using a Pkcs8PrivateBlob.
    var cngKey = CngKey.Import(Convert.FromBase64String(privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);

    //Create new ECDsaCng object with the imported key.
    var ecDsaCng = new ECDsaCng(cngKey);
    ecDsaCng.HashAlgorithm = CngAlgorithm.ECDsaP256;

    //Create new SigningCredentials instance which will be used for signing the token.
    var signingCredentials = new SigningCredentials(new ECDsaSecurityKey(ecDsaCng), SecurityAlgorithms.EcdsaSha256);

    var now = DateTime.UtcNow;

    //Create new list with the required claims.
    var claims = new List<Claim>
    {
        new Claim("iss", teamId),
        new Claim("iat", EpochTime.GetIntDate(now).ToString(), ClaimValueTypes.Integer64),
        new Claim("exp", EpochTime.GetIntDate(now.AddMinutes(5)).ToString(), ClaimValueTypes.Integer64),
        new Claim("aud", "https://appleid.apple.com"),
        new Claim("sub", clientId)
    };

    //Create the JSON Web Token object.
    var token = new JwtSecurityToken(
        issuer: teamId,
        claims: claims,
        expires: now.AddMinutes(5),
        signingCredentials: signingCredentials);

    token.Header.Add("kid", keyId);

    //Return the JSON Web Token as a string.
    return tokenHandler.WriteToken(token);
}

Then to validate the token I have this

public async Task<AppleVerifySignInTokenResponse> ValidateSignInToken(...)
{
    try
    {
        using (var httpClient = new HttpClient())
        {
            httpClient.BaseAddress = "https://appleid.apple.com/auth/token";
            var jsonItem = JsonConvert.SerializeObject(new
            {
                client_id = "com.appname.appnameservice",
                client_secret = GenerateAppleClientSecret(),
                code = authorizationCode, // AuthorizationCode from frontend
                grant_type = "authorization_code",
                redirect_uri = "https://myredirecturi.com" // identical to the one in developer portal
            });
            var httpContent = new StringContent(jsonItem, Encoding.UTF8, "application/x-www-form-urlencoded");
            var response = await httpClient.PostAsync("", httpContent).ConfigureAwait(false);
            if (response.IsSuccessStatusCode == true && response.Content != null)
            {
                var json = response.Content.ReadAsStringAsync().Result;
                return JsonConvert.DeserializeObject<AppleVerifySignInTokenResponse>(json);
            }

            return null;
        }
    }
    catch (Exception ex)
    {
        return null;
    }

}

The response from above keeps returning "invalid_client" no matter what I do... I have tried changing the clientId from "com.appname.appnameservice" to "com.appname.appname", in some of the places and all of the places. I have tried generating a new .p8 file and using that.

Any Ideas? I have spent probably a week on this :'( Thanks!

Note: The private key has a bunch of periods in it because I wanted to redact most of the content. I will generate a new one once I get this working.

Also, this is kind of insane. If apple is going to require us to support apple sign in, they need better documentation and error messages. There seems to be so many developers lost on what to do with very little success.

Answered by jwags in 683857022

So, I finally solved this. Hopefully it helps someone in the future. I think the issue was my lack of understanding how to properly set the header in an HttpClient.

My new method to validate the Token is

public async Task<AppleVerifySignInTokenResponse> ValidateSignInToken(...)
{
    try
    {
        using (var httpClient = new HttpClient())
        {
            httpClient.BaseAddress = "https://appleid.apple.com/auth/token";
            var dictionary = new Dictionary<string, string>
            {
                { "client_id", "com.appname.appname" },
                { "client_secret", GenerateAppleClientSecret() },
                { "code", authorizationCode },
                { "grant_type", "authorization_code" },
                { "redirect_uri", "https://myredirecturi.com" },
            });
            using (var content = new FormUrlEncodedContent(dictionary))
            {
                content.Headers.Clear();
                content.Headers.Add("Content-Type", "application/x-www-form-urlencoded");

                var response = await httpClient.PostAsync("", content).ConfigureAwait(false);
                if (response.IsSuccessStatusCode == true && response.Content != null)
                {
                    var json = response.Content.ReadAsStringAsync().Result;
                    return JsonConvert.DeserializeObject<AppleVerifySignInTokenResponse>(json);
                }

                return null;
            }
        }
    }
    catch (Exception ex)
    {
        return null;
    }
}

Also, note that I was struggling to figure out what my client_id should be. I have my app (com.appname.appname) and my service (com.appname.appnameserice). I did not use the service one anywhere.

This is what I use to generate the client_secret

public string GenerateAppleClientSecret()
{
    // Content of the .p8 file (without -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----).
    string privateKey = "MIG...";

    // The 10-character key identifier from the portal (Also the name of the .p8 file. If file is AuthKey_12345.p8, put 12345)
    string keyId = _KeyId;

    // The Bundle Id of the iOS app (found in app store connect -> app -> App Information)
    string clientId = "com.appname.appname"; 

    // Found in developer portal
    string teamId = "SLT12345"; 

    var now = DateTimeOffset.UtcNow;

    //Import the key using a Pkcs8PrivateBlob.
    var cngKey = CngKey.Import(Convert.FromBase64String(privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);

    //Create new ECDsaCng object with the imported key.
    var ecDsaCng = new ECDsaCng(cngKey);
    ecDsaCng.HashAlgorithm = CngAlgorithm.ECDsaP256;

    //Create new SigningCredentials instance which will be used for signing the token.
    var signingCredentials = new SigningCredentials(new ECDsaSecurityKey(ecDsaCng), SecurityAlgorithms.EcdsaSha256);

    var tokenHandler = new JwtSecurityTokenHandler();
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Issuer = teamId,
        Subject = new ClaimsIdentity(new Claim[]
        {
            new Claim("iss", teamId),
            new Claim("iat", now.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64),
            new Claim("exp", now.AddDays(7).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64),
            new Claim("aud", "https://appleid.apple.com"),
            new Claim("sub", clientId)
        }),
        Expires = DateTime.UtcNow.AddMinutes(5),
        SigningCredentials = signingCredentials,
    };

    var token = tokenHandler.CreateJwtSecurityToken(tokenDescriptor);
    if (commandModel.Kid.IsNullOrWhiteSpace() == false)
    {
        token.Header.Add("kid", keyId);
    }

    return tokenHandler.WriteToken(token);
}

Here are some various tutorials I have followed:

  • https://accedia.com/blog/dotnetifying-sign-in-with-apple/
  • https://www.scottbrady91.com/OpenID-Connect/Implementing-Sign-In-with-Apple-in-ASPNET-Core

I just followed this guide to the T and it didn't help either.

https://fluffy.es/how-to-solve-invalid_client-error-in-sign-in-with-apple/

I also verified the JWT at https://jwt.io/#debugger

I still get { "error": "invalid_client"}

Accepted Answer

So, I finally solved this. Hopefully it helps someone in the future. I think the issue was my lack of understanding how to properly set the header in an HttpClient.

My new method to validate the Token is

public async Task<AppleVerifySignInTokenResponse> ValidateSignInToken(...)
{
    try
    {
        using (var httpClient = new HttpClient())
        {
            httpClient.BaseAddress = "https://appleid.apple.com/auth/token";
            var dictionary = new Dictionary<string, string>
            {
                { "client_id", "com.appname.appname" },
                { "client_secret", GenerateAppleClientSecret() },
                { "code", authorizationCode },
                { "grant_type", "authorization_code" },
                { "redirect_uri", "https://myredirecturi.com" },
            });
            using (var content = new FormUrlEncodedContent(dictionary))
            {
                content.Headers.Clear();
                content.Headers.Add("Content-Type", "application/x-www-form-urlencoded");

                var response = await httpClient.PostAsync("", content).ConfigureAwait(false);
                if (response.IsSuccessStatusCode == true && response.Content != null)
                {
                    var json = response.Content.ReadAsStringAsync().Result;
                    return JsonConvert.DeserializeObject<AppleVerifySignInTokenResponse>(json);
                }

                return null;
            }
        }
    }
    catch (Exception ex)
    {
        return null;
    }
}

Also, note that I was struggling to figure out what my client_id should be. I have my app (com.appname.appname) and my service (com.appname.appnameserice). I did not use the service one anywhere.

This is what I use to generate the client_secret

public string GenerateAppleClientSecret()
{
    // Content of the .p8 file (without -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----).
    string privateKey = "MIG...";

    // The 10-character key identifier from the portal (Also the name of the .p8 file. If file is AuthKey_12345.p8, put 12345)
    string keyId = _KeyId;

    // The Bundle Id of the iOS app (found in app store connect -> app -> App Information)
    string clientId = "com.appname.appname"; 

    // Found in developer portal
    string teamId = "SLT12345"; 

    var now = DateTimeOffset.UtcNow;

    //Import the key using a Pkcs8PrivateBlob.
    var cngKey = CngKey.Import(Convert.FromBase64String(privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);

    //Create new ECDsaCng object with the imported key.
    var ecDsaCng = new ECDsaCng(cngKey);
    ecDsaCng.HashAlgorithm = CngAlgorithm.ECDsaP256;

    //Create new SigningCredentials instance which will be used for signing the token.
    var signingCredentials = new SigningCredentials(new ECDsaSecurityKey(ecDsaCng), SecurityAlgorithms.EcdsaSha256);

    var tokenHandler = new JwtSecurityTokenHandler();
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Issuer = teamId,
        Subject = new ClaimsIdentity(new Claim[]
        {
            new Claim("iss", teamId),
            new Claim("iat", now.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64),
            new Claim("exp", now.AddDays(7).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64),
            new Claim("aud", "https://appleid.apple.com"),
            new Claim("sub", clientId)
        }),
        Expires = DateTime.UtcNow.AddMinutes(5),
        SigningCredentials = signingCredentials,
    };

    var token = tokenHandler.CreateJwtSecurityToken(tokenDescriptor);
    if (commandModel.Kid.IsNullOrWhiteSpace() == false)
    {
        token.Header.Add("kid", keyId);
    }

    return tokenHandler.WriteToken(token);
}

Another thing to note. You can only attempt to validate the AuthorizationCode once. Attempting to send it twice will result in the "invalid_grant" error.

Also, the way I discovered that c# was broken and nothing else was that I attempted to execute the call using curl.

curl -i POST "https://appleid.apple.com/auth/token" \
-H "content-type: application/x-www-form-urlencoded" \
-d "client_id=com.appname.appname" \
-d "client_secret=JWT_GENERATED_IN_C#" \
-d "code=AUTHORIZATION_CODE_FROM_APP" \
-d "grant_type=authorization_code" \
-d "redirect_uri=https://myredirecturi.com"
Apple Sign In - invalid_client
 
 
Q