lcobucci / jwt

A simple library to work with JSON Web Token and JSON Web Signature
https://lcobucci-jwt.readthedocs.io/en/stable/
BSD 3-Clause "New" or "Revised" License
7.29k stars 600 forks source link

Connect to Apple using .p8 cert file ( PHP 8.1 ) #819

Closed cubevis closed 2 years ago

cubevis commented 2 years ago

Hello!

Im using this liblary to generate ES256 JWT for Apple to connect with them, but the problem is that generated token is invalid.

So in summary :

What I did and it didn't work

My Code (PHP 8.1)

require 'vendor/autoload.php';
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Validation\NoConstraintsGiven;
use Lcobucci\JWT\Signer\Key\InMemory;

$privateKey = InMemory::file(__DIR__ . '/AuthKey_XXXXXXX.pem');
$publicKey = InMemory::file(__DIR__ . '/AuthKey_XXXXXXX.pub');

$config = Configuration::forAsymmetricSigner(Lcobucci\JWT\Signer\Ecdsa\Sha256::create(), $privateKey, $publicKey );

$now = new DateTimeImmutable();

$token = $config->builder()
    ->issuedBy('XXXXXXXXX')
    ->withHeader('alg', 'ES256')
    ->withHeader('kid', 'XXXXXXXXX')
    ->withHeader('typ', 'JWT')
    ->permittedFor('appstoreconnect-v1')
    ->withClaim('scope', array("GET /v1/apps?filter[platform]=IOS"))
    ->issuedAt($now)
    ->expiresAt($now->modify('+1 hour'))
    ->getToken($config->signer(), $config->signingKey());

$final_token = $token->toString();

// And final connecting with Apple because token is generated :
$url = 'https://api.appstoreconnect.apple.com/v1/apps/';
$ch = curl_init();
$header = array();
$header[] = 'Content-length: 0';
$header[] = 'Content-type: application/json';
$header[] = 'Authorization: Bearer '.$final_token;
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_HEADER, TRUE);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$head = curl_exec($ch);
print_r($head);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

What I need :

Thanks, Jake

Ocramius commented 2 years ago

Does your JWT token look valid, according to the library itself? What is the response from the curl request?

cubevis commented 2 years ago

@Ocramius, thanks for response, the response from curl :

HTTP/1.1 401 Unauthorized Server: daiquiri/3.0.0 Date: Wed, 02 Feb 2022 09:33:14 GMT Content-Type: application/json Content-Length: 350 Connection: keep-alive Strict-Transport-Security: max-age=31536000; includeSubDomains X-Apple-Jingle-Correlation-Key: XXXXXXXXXXXXXX x-daiquiri-instance: daiquiri:18493001:mr85p00it-hyhk03154801:7987:21RELEASE207:daiquiri-amp-all-shared-ext-001-mr { "errors": [{ "status": "401", "code": "NOT_AUTHORIZED", "title": "Authentication credentials are missing or invalid.", "detail": "Provide a properly configured and signed bearer token, and make sure that it has not expired. Learn more about Generating Tokens for API Requests https://developer.apple.com/go/?id=api-generating-tokens" }] },

The token looks valid but when I paste this to let's say : jwt.io, it says: Invalid Signature

Ocramius commented 2 years ago

I paste this to let's say : jwt.io, it says: Invalid Signature

The signature needs to be validated against your key - I don't suggest pasting your key into jwt.io, but to validate it locally against the Parser provided by this library.

cubevis commented 2 years ago

@Ocramius, so according to liblary: Parser, how to validate my generated JWT using PHP? I will dive into liblary right now, but if are more familiar with this, please dive with me :)

lcobucci commented 2 years ago

How did you convert the p8 certificate? Is this library able to verify the signature using the converted public key? Does Apple mandate any claim that you're not setting (sub)?

lcobucci commented 2 years ago

It also looks like you're using the date formatter that uses microseconds as precision (when the time object provides it). Does apple support that?

If they don't, try using the ChainedFormatter::withUnixTimestampDates() as formatter.

cubevis commented 2 years ago

@lcobucci , many thanks for response! :)

I converted .p8 using this line in terminal : openssl pkcs8 -nocrypt -in AuthKey.p8 -out AuthKey.pem But this didn't of course get me public key, so I tried another one :

curl -OL https://github.com/web-token/jwt-app/raw/gh-pages/jose.phar
curl -OL https://github.com/web-token/jwt-app/raw/gh-pages/jose.phar.pubkey

chmod +x jose.phar

./jose.phar key:load:key ./AuthKey_2CMKR9X24G.p8 > private_key.jwk
./jose.phar key:convert:public $(cat private_key.jwk) > public_key.jwk

./jose.phar key:convert:pkcs1 $(cat private_key.jwk) > private_key.pem
./jose.phar key:convert:pkcs1 $(cat public_key.jwk) > public_key.pem

rm *.jwk
rm jose.phar*

This code finally gave me private.pem and public.pem

In terms of microseconds, could u explain more where should I do this?

Many thanks, Jake

lcobucci commented 2 years ago

This is how I generate and convert ECDSA keys directly via openssl:

openssl genpkey -algorithm EC -out private-key.pem \
    -pkeyopt ec_paramgen_curve:P-256 \
    -pkeyopt ec_param_enc:named_curve

openssl ec -in private-key.pem -pubout -out public-key.pem
Sample Keys ## Private key ``` -----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgItea/N8Krs4nl1yh KDXfgfNqd4IlCB6T1Ik4A7B7w8qhRANCAAQUpGnJ2Ok0ioJQ9eK0uKL2K8D1am+h hBvtuKyn9Tr0ORHiuVrwOBu8Mlu0+TGKIrh7YhyGpIJvHC5sfBXKDxWY -----END PRIVATE KEY----- ``` ## Public key ``` -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFKRpydjpNIqCUPXitLii9ivA9Wpv oYQb7bisp/U69DkR4rla8DgbvDJbtPkxiiK4e2IchqSCbxwubHwVyg8VmA== -----END PUBLIC KEY----- ```

With that, I'm able to issue and verify tokens:

<?php
require 'vendor/autoload.php';

use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Validation\Constraint\SignedWith;

$config = Configuration::forAsymmetricSigner(
    Signer\Ecdsa\Sha256::create(),
    InMemory::file(__DIR__ . '/private-key.pem'),
    InMemory::file(__DIR__ . '/public-key.pem'),
);

$now = new DateTimeImmutable();

$token = $config->builder()
    ->issuedBy('57246542-96fe-1a63-e053-0824d011072a')
    ->permittedFor('appstoreconnect-v1')
    ->withClaim('scope', array("GET /v1/apps?filter[platform]=IOS"))
    ->issuedAt($now)
    ->expiresAt($now->modify('+1 hour'))
    ->getToken($config->signer(), $config->signingKey());

var_dump(
    $config->validator()->validate(
        $token,
        new SignedWith($config->signer(), $config->verificationKey())
    )
);

echo $token->toString(), PHP_EOL;

The expected output here is something like:

bool(true)
eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiI1NzI0NjU0Mi05NmZlLTFhNjMtZTA1My0wODI0ZDAxMTA3MmEiLCJhdWQiOiJhcHBzdG9yZWNvbm5lY3QtdjEiLCJzY29wZSI6WyJHRVQgL3YxL2FwcHM_ZmlsdGVyW3BsYXRmb3JtXT1JT1MiXSwiaWF0IjoxNjQzODIzMTg5LjE3ODc4MSwiZXhwIjoxNjQzODI2Nzg5LjE3ODc4MX0.zhslOHOZYNZdoztRyq2qvCJr_wrVA-LABhfiOZFrrmw6YOAxuPZWINp_Ueq3DEGssp_o5l-tyo1hN912oJagwQ

Taking that token and public key to jwt.io gives you this:

image

As you can see, the signature is also properly verified on jwt.io.

Now, check that the timestamps on iat and exp claims have decimal places. That's because the DateTimeImmutable object has microsecond precision. If you don't want that, you can make the following adjustment on the script:

  use Lcobucci\JWT\Configuration;
  use Lcobucci\JWT\Signer;
  use Lcobucci\JWT\Signer\Key\InMemory;
+ use Lcobucci\JWT\Encoding\ChainedFormatter;
  use Lcobucci\JWT\Validation\Constraint\SignedWith;

(...)

- $token = $config->builder()
+ $token = $config->builder(ChainedFormatter::withUnixTimestampDates())
      ->issuedBy('57246542-96fe-1a63-e053-0824d011072a')

(...)

You'll have a token like this:

eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiI1NzI0NjU0Mi05NmZlLTFhNjMtZTA1My0wODI0ZDAxMTA3MmEiLCJhdWQiOiJhcHBzdG9yZWNvbm5lY3QtdjEiLCJzY29wZSI6WyJHRVQgL3YxL2FwcHM_ZmlsdGVyW3BsYXRmb3JtXT1JT1MiXSwiaWF0IjoxNjQzODIzNjY0LCJleHAiOjE2NDM4MjcyNjR9.2cAA0xlk5nqdeOjAOleX6ViChE2NttT0iX_G_rNMWULLjP9PkjETJQoxq6-ZmqUD38TnmKp8B7hxeB9HOmRLkA

Which would give you the following result on jwt.io

image


I also tested the PKCS8 -> PKCS1 conversion and public key extraction only with openssl using my script and got the same results (even on jwt.io)...

openssl pkcs8 -in AuthKey.p8 -out private-key.pem -nocrypt

openssl ec -in private-key.pem -pubout -out public-key.pem

I advise you to try it out (with my script and your key) and verify that it works fine on jwt.io and then trying it out with Apple.

I hope this helps you 😄

lcobucci commented 2 years ago

Ahhh, if your key is in DER format you should use this:

openssl pkcs8 -inform DER -in AuthKey.p8 -out private-key.pem -nocrypt
cubevis commented 2 years ago

@lcobucci again many thanks for response! Let me test it and I will let u know asap! :)

cubevis commented 2 years ago

@lcobucci, soo I tried and let me tell what we got :

Buuut, still for some reason Apple says : 401, Unauthorized which may be problem in somewhere else :/

So in summary :

My curl code :

$final_token = $token->toString();
$url = 'https://api.appstoreconnect.apple.com/v1/apps/';
$ch = curl_init();
$header = array();
$header[] = 'Content-length: 0';
$header[] = 'Content-type: application/json';
$header[] = 'Authorization: Bearer '.$final_token;
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_HEADER, TRUE);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$head = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

Maybe Apple doesn't like this .pem convert way? Maybe they want to use .p8 I'm pretty sure that this issue will be common :/

cubevis commented 2 years ago

@lcobucci, this is what I also found : https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests Few important information, for example :

lcobucci commented 2 years ago

@lcobucci, this is what I also found : https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests Few important information, for example :

  • iat: The token’s creation time, in UNIX epoch time; for example, 1528407600.

And did you try to change the claims formatter as I suggested?

cubevis commented 2 years ago

Yes, still not works :(

cubevis commented 2 years ago

I wrote to Apple about this, because as u can see, you helped me to finally generate JWT which has signature verified but Apple doesn't like that :/

lcobucci commented 2 years ago

@cubevis is that for a development environment? If not, please make sure to revoke it. Don't post this publicly to avoid compromising your credentials.

cubevis commented 2 years ago

@lcobucci it is, but if we can solve this, i will of course disable this and generate new one :)

lcobucci commented 2 years ago

Did you see this:

The token’s expiration time in Unix epoch time. Tokens that expire more than 20 minutes into the future are not valid except for resources listed in Determine the Appropriate Token Lifetime.

Your script was issuing tokens with 1h of expiration.

cubevis commented 2 years ago

@lcobucci IT WORKS ! ! ! !

cubevis commented 2 years ago

Jessuuuuuus.... :D :D

cubevis commented 2 years ago

@lcobucci, many thanks for your help! I think this topic should be available for everyone who :

lcobucci commented 2 years ago

It would be great if you could send a PR to the docs or create a discussion to share your findings.

Some remarks to make, though:

  1. since you're only issuing tokens you probably don't need the public key (only the builder, signer, and signature key are required)
  2. The key you pasted seemed like a PKCS#8 in PEM format. That shouldn't require any conversion to work with ext-openssl (I tested a similar key with PHP 8.1 and it just works)
cubevis commented 2 years ago

@lcobucci let me deal with it and give feedback asap! :)