I was challenged to implement Apple sign-in functionality. At first, this task seemed like an easy one. Another OAuth provider I thought. Then I discovered that it requires client_secret to be generated from the p8 certificate. The entire workflow is well described in this article.
The private keys must be generated on the Apple Developer Portal.
- Create an App ID.
- Then, for the app, create a service ID.
- Finally, generate the keys for your app.
I tried first the GeneaLabs/laravel-sign-in-with-apple library. They suggest using a ruby script to generate the client secret.
require 'jwt'
key_file = 'key.txt'
team_id = ''
client_id = ''
key_id = ''
ecdsa_key = OpenSSL::PKey::EC.new IO.read key_file
headers = {
'kid' => key_id
}
claims = {
'iss' => team_id,
'iat' => Time.now.to_i,
'exp' => Time.now.to_i + 86400*180,
'aud' => 'https://appleid.apple.com',
'sub' => client_id,
}
token = JWT.encode claims, ecdsa_key, 'ES256', headers
puts token
Surprisingly, after 6 months since the client_secret was generated, I started getting an error:
Client error: POST https://appleid.apple.com/auth/token resulted in a 400 Bad Request response: {“error”:”invalid_client”}
It turned out that the client secret was expired because Apple restricted its maximum lifetime to 6 months.
I started looking for other solutions and decided to use SocialiteProviders/Apple and generate the client secret on the fly, it’s described in this article. The code looks like this:
<?php
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
$now = new \DateTimeImmutable();
$jwtConfig = Configuration::forSymmetricSigner(
new Sha256(),
InMemory::file(__DIR__ . '/AuthKey.p8')
);
$token = $jwtConfig->builder()
->issuedBy('XXXXXXXX')
->issuedAt($now)
->expiresAt($now->modify('+1 hour'))
->permittedFor('https://appleid.apple.com')
->relatedTo('com.example.service-id')
->withHeader('kid', 'XXXXXXXX')
->getToken($jwtConfig->signer(), $jwtConfig->signingKey());
echo $token->toString();
The idea seemed great: generate the secret on each request automatically rather than manually every 6 months. But there was an issue: the p8 certificate that I downloaded from Apple wasn’t working with Lcobucci\JWT. I was getting the following error:
It was not possible to parse your key, reason: error:0909006C:PEM routines:get_name:no start line
It turned out that the p8 (PKSC#8) key needs to be converted to PKCS#1 standard. After a while a figured out the solution:
openssl pkcs8 -nocrypt -in AuthKey_XXXXXXXXXX.p8 -traditional -out AuthKey.pem
So I was able to load this the generated key either as a file
$configuration = Configuration::forSymmetricSigner(
new Sha256(),
InMemory::file(__DIR__ . '/AuthKey.pem)
);
Or convert it to base64 and use it as an ENV variable:
base64 -w 0 < ./AuthKey.pem
$configuration = Configuration::forSymmetricSigner(
new Sha256(),
InMemory::base64Encoded('Paste=here=the=base64=output===')
);