Apple JWT Authentication Issues with PHP ( error invalid_client )

Asked

Viewed 35 times

-1

This is not necessarily a question, but a solution I want share with the community about a problem I’ve had recently and I can’t answer on other topics for lack of score.

Case: an iOS app with Apple ID login and PHP backend.

Problem: PHP libraries that use Openssl to create JWT do not support the ES256 algorithm that Apple uses because of a limitation of Openssl itself. This causes the error of using native libraries {"error":"invalid_client"}

1 answer

0

Solution:

This explanation is an addition to the tutorial that exists in https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple . If you don’t understand something please ask me to clarify and, if necessary, correct the reply.

First I installed Ruby on the server with the JWT dependency. In it I created the following script:

require 'jwt'
require 'json'

key_file = '../configs/applekey_12345ABCDE.p8'
team_id = '12345ABCDE'
key_id = 'ABCDE12345'

client_id = 'br.com.myapp'

headers = {
  'kid' => key_id
}

ecdsa_key = OpenSSL::PKey::EC.new IO.read key_file


claims = {
    'iss' => team_id,
    'iat' => Time.now.to_i,
    'exp' => Time.now.to_i + 15777000,
    'aud' => 'https://appleid.apple.com',
    'sub' => client_id,
}

token = JWT.encode claims, ecdsa_key, 'ES256', headers

File.write('native_token.txt', token)

client_id = 'br.com.myapp.signin'

claims = {
    'iss' => team_id,
    'iat' => Time.now.to_i,
    'exp' => Time.now.to_i + 12777000,
    'aud' => 'https://appleid.apple.com',
    'sub' => client_id,
}

token = JWT.encode claims, ecdsa_key, 'ES256', headers

File.write('legacy_token.txt', token)

The above script creates two distinct files: the first is for native login (which is used in iOS 13 and higher) and the other is for legacy (iOS 12 and lower, which occurs via Webview).

For these two login types to occur the app programmer will have to register the two names (the Bundle id and the service id), where the Bundle id is used for the Native and the service id is for the legacy.

It is generating code with 6 months validity, and a CRON on the server renews it monthly, superimposing on the same files.

Once generated these two Txts PHP will consume them to make the request to Apple and thus fetch the user’s email information in this way:

function getUserData($code,$is_legacy){
        $key = preg_replace( "/\r|\n/", "", file_get_contents('ruby/'.($is_legacy=='native'?'native':'legacy').'_token.txt'));
        $client_id = 'br.com.myapp'.($is_legacy=='native'?'':'.signin');            
        $data = array(
                    'client_id' => $client_id,
                    'client_secret' => $key,
                    'code' => $code,
                    'grant_type' => 'authorization_code'
                    );
                    
        $ch = curl_init();
        $headers = array
        (
            'Content-Type: application/x-www-form-urlencoded'
        );
        curl_setopt( $ch,CURLOPT_HTTPHEADER, $headers );
        curl_setopt( $ch, CURLOPT_URL, 'https://appleid.apple.com/auth/token');
        curl_setopt( $ch, CURLOPT_POST, true);
        curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query($data) );
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );

        $serverOutput = json_decode(curl_exec($ch),true);

        curl_close ($ch);

        $user_email = json_decode(base64_decode(explode('.',$serverOutput['id_token'])[1]),true)['email'];
        // FAÇA AQUI ALGO COM O $user_email 
}

Explaining in kids: $key receives the contents of the file generated by the Ruby script through a file_get_contents (consider that it should point to where the TXT is). It takes the client_id according to the type of authentication and the $code the app receives when authentication is authorized. So he sends everything to Apple.

The return of this is a JSON which, within the id_token object, nothing else is various data in Base64 separated by points. Here just take the object in the second position, "decrypt" it and thereby get the JSON with the data where the email is perhaps the only one that really matters, since the name and surname are delivered to the app as soon as it gets the authorization (and the app should pass it to the backend along with the code for you to save/update it to your database).

Browser other questions tagged

You are not signed in. Login or sign up in order to post.