From 5641e50bac17b44212d0bfc72ded967daa8fdd06 Mon Sep 17 00:00:00 2001 From: Jan Hamrak Date: Thu, 4 May 2023 07:51:53 +0200 Subject: [PATCH 1/7] no message --- src/Apple/AppleSignerNone.php | 26 ++++++++++++++++++++++++++ src/Apple/Provider.php | 5 ++++- src/Apple/composer.json | 3 ++- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/Apple/AppleSignerNone.php diff --git a/src/Apple/AppleSignerNone.php b/src/Apple/AppleSignerNone.php new file mode 100644 index 000000000..4bb82ae15 --- /dev/null +++ b/src/Apple/AppleSignerNone.php @@ -0,0 +1,26 @@ +parser()->parse($jwt); $data = Cache::remember('socialite:Apple-JWKSet', 5 * 60, function () { diff --git a/src/Apple/composer.json b/src/Apple/composer.json index c951e9506..4ad22184c 100644 --- a/src/Apple/composer.json +++ b/src/Apple/composer.json @@ -40,7 +40,8 @@ "ext-json": "*", "ext-openssl": "*", "firebase/php-jwt": "^6.2", - "lcobucci/jwt": "^4.1.5", + "lcobucci/clock": "^2.0 || ^3.0", + "lcobucci/jwt": "^4.1.5 || ^5.0.0", "socialiteproviders/manager": "~4.0" }, "suggest": { From 37df282a27a97021ef16324984ca260146155295 Mon Sep 17 00:00:00 2001 From: Jan Hamrak Date: Thu, 4 May 2023 07:55:07 +0200 Subject: [PATCH 2/7] Fix StyleCI --- src/Apple/AppleSignerNone.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Apple/AppleSignerNone.php b/src/Apple/AppleSignerNone.php index 4bb82ae15..34484b8c7 100644 --- a/src/Apple/AppleSignerNone.php +++ b/src/Apple/AppleSignerNone.php @@ -1,4 +1,5 @@ Date: Thu, 4 May 2023 11:18:19 +0200 Subject: [PATCH 3/7] Add option for private key instead of secret key. --- src/Apple/AppleToken.php | 32 ++++++++++++++ src/Apple/Provider.php | 95 ++++++++++++++++++++++++++++++++++------ src/Apple/README.md | 18 ++++++++ 3 files changed, 132 insertions(+), 13 deletions(-) create mode 100644 src/Apple/AppleToken.php diff --git a/src/Apple/AppleToken.php b/src/Apple/AppleToken.php new file mode 100644 index 000000000..b93791e67 --- /dev/null +++ b/src/Apple/AppleToken.php @@ -0,0 +1,32 @@ +jwtConfig = $jwtConfig; + } + + public function generate(): string + { + $now = CarbonImmutable::now(); + + $token = $this->jwtConfig->builder() + ->issuedBy(config('services.apple.team_id')) + ->issuedAt($now) + ->expiresAt($now->addHour()) + ->permittedFor(Provider::URL) + ->relatedTo(config('services.apple.client_id')) + ->withHeader('kid', config('services.apple.key_id')) + ->getToken($this->jwtConfig->signer(), $this->jwtConfig->signingKey()); + + return $token->toString(); + } +} diff --git a/src/Apple/Provider.php b/src/Apple/Provider.php index ff8c6b940..71e09ccff 100644 --- a/src/Apple/Provider.php +++ b/src/Apple/Provider.php @@ -5,6 +5,7 @@ use Firebase\JWT\JWK; use GuzzleHttp\Client; use GuzzleHttp\RequestOptions; +use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; @@ -24,7 +25,7 @@ class Provider extends AbstractProvider { public const IDENTIFIER = 'APPLE'; - private const URL = 'https://appleid.apple.com'; + public const URL = 'https://appleid.apple.com'; /** * {@inheritdoc} @@ -46,6 +47,20 @@ class Provider extends AbstractProvider */ protected $scopeSeparator = ' '; + /** + * JWT Configuration. + * + * @var ?Configuration + */ + protected $jwtConfig = null; + + /** + * Private Key. + * + * @var string + */ + protected $privateKey = ''; + /** * {@inheritdoc} */ @@ -89,7 +104,7 @@ protected function getCodeFields($state = null) public function getAccessTokenResponse($code) { $response = $this->getHttpClient()->post($this->getTokenUrl(), [ - RequestOptions::HEADERS => ['Authorization' => 'Basic '.base64_encode($this->clientId.':'.$this->clientSecret)], + RequestOptions::HEADERS => ['Authorization' => 'Basic '.base64_encode($this->clientId.':'.$this->getClientSecret())], RequestOptions::FORM_PARAMS => $this->getTokenFields($code), ]); @@ -111,12 +126,51 @@ protected function getTokenFields($code) */ protected function getUserByToken($token) { - static::verify($token); + $this->checkToken($token); $claims = explode('.', $token)[1]; return json_decode(base64_decode($claims), true); } + protected function getClientSecret() + { + if (!$this->jwtConfig) { + $this->getJwtConfig(); // Generate Client Secret from private key if not set. + } + + return $this->clientSecret; + } + + protected function getJwtConfig() + { + if (!$this->jwtConfig) { + $private_key_path = config('services.apple.private_key', ''); + $private_key_passphrase = config('services.apple.passphrase', ''); + $signer = config('services.apple.signer', ''); + + if (empty($signer) || !class_exists($signer)) { + $signer = !empty($private_key_path) ? \Lcobucci\JWT\Signer\Ecdsa\Sha256::class : AppleSignerNone::class; + } + + if (!empty($private_key_path) && file_exists($private_key_path)) { + $this->privateKey = file_get_contents($private_key_path); + } + + $this->jwtConfig = Configuration::forSymmetricSigner( + new $signer(), + InMemory::plainText($this->privateKey, $private_key_passphrase) + ); + + if (!empty($this->privateKey)) { + $appleToken = new AppleToken($this->getJwtConfig()); + $this->clientSecret = $appleToken->generate(); + config()->set('services.apple.client_secret', $this->clientSecret); + } + } + + return $this->jwtConfig; + } + /** * Return the user given the identity token provided on the client * side by Apple. @@ -135,7 +189,7 @@ public function userByIdentityToken(string $token): SocialiteUser } /** - * Verify Apple jwt. + * Verify Apple JWT. * * @param string $jwt * @@ -143,13 +197,9 @@ public function userByIdentityToken(string $token): SocialiteUser * * @see https://appleid.apple.com/auth/keys */ - public static function verify($jwt) + public function checkToken($jwt) { - $jwtContainer = Configuration::forSymmetricSigner( - new AppleSignerNone(), - InMemory::plainText('') - ); - $token = $jwtContainer->parser()->parse($jwt); + $token = $this->getJwtConfig()->parser()->parse($jwt); $data = Cache::remember('socialite:Apple-JWKSet', 5 * 60, function () { $response = (new Client())->get(self::URL.'/auth/keys'); @@ -169,7 +219,7 @@ public static function verify($jwt) ]; try { - $jwtContainer->validator()->assert($token, ...$constraints); + $this->jwtConfig->validator()->assert($token, ...$constraints); return true; } catch (RequiredConstraintsViolated $e) { @@ -180,6 +230,25 @@ public static function verify($jwt) throw new InvalidStateException('Invalid JWT Signature'); } + /** + * Verify Apple jwt via static function. + * + * @param string $jwt + * + * @return bool + * + * @see https://appleid.apple.com/auth/keys + */ + public static function verify($jwt) + { + return (new self( + new Request(), + config('services.apple.client_id'), + config('services.apple.client_secret'), + config('services.apple.redirect') + ))->checkToken($jwt); + } + /** * {@inheritdoc} */ @@ -278,9 +347,9 @@ protected function getRevokeUrl(): string public function revokeToken(string $token, string $hint = 'access_token') { return $this->getHttpClient()->post($this->getRevokeUrl(), [ - RequestOptions::FORM_PARAMS => [ + RequestOptions::FORM_PARAMS => [ 'client_id' => $this->clientId, - 'client_secret' => $this->clientSecret, + 'client_secret' => $this->getClientSecret(), 'token' => $token, 'token_type_hint' => $hint, ], diff --git a/src/Apple/README.md b/src/Apple/README.md index e750edf4a..d01f1b05b 100644 --- a/src/Apple/README.md +++ b/src/Apple/README.md @@ -18,6 +18,24 @@ Please see the [Base Installation Guide](https://socialiteproviders.com/usage/), ], ``` +If using a private key file, add lines to the configuration as follows: + +```php +'apple' => [ + 'client_id' => env('APPLE_CLIENT_ID'), + 'client_secret' => env('APPLE_CLIENT_SECRET'), // Empty if using private key. + 'key_id' => env('APPLE_KEY_ID'), + 'team_id' => env('APPLE_TEAM_ID'), + 'private_key' => env('APPLE_PRIVATE_KEY'), // Must be absolute path, e.g. /var/www/cert/AuthKey_XYZ.p8 + 'passphrase' => env('APPLE_PASSPHRASE'), // Set if your key have a passphrase. + 'signer' => env('APPLE_SIGNER'), // Signer used for Configuration::forSymmetricSigner(). + 'redirect' => env('APPLE_REDIRECT_URI') +], +``` + +If you receive error `400 Bad Request {"error":"invalid_client"}` , a possible solution is to use another signer, e.g. "\Lcobucci\JWT\Signer\Ecdsa\Sha256". + + See [Configure Apple ID Authentication](https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple) > Note: the client secret used for "Sign In with Apple" is a JWT token that can have a maximum lifetime of 6 months. The article above explains how to generate the client secret on demand and you'll need to update this every 6 months. To generate the client secret for each request, see [Generating A Client Secret For Sign In With Apple On Each Request](https://bannister.me/blog/generating-a-client-secret-for-sign-in-with-apple-on-each-request) From cab4fb551cb271856694bee143e45d0b025033e2 Mon Sep 17 00:00:00 2001 From: Jan Hamrak Date: Thu, 4 May 2023 16:24:10 +0200 Subject: [PATCH 4/7] FIX ERROR: Key cannot be empty --- src/Apple/AppleSignerInMemory.php | 36 +++++++++++++++++++++++++++++++ src/Apple/Provider.php | 5 ++--- 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 src/Apple/AppleSignerInMemory.php diff --git a/src/Apple/AppleSignerInMemory.php b/src/Apple/AppleSignerInMemory.php new file mode 100644 index 000000000..7d6030da1 --- /dev/null +++ b/src/Apple/AppleSignerInMemory.php @@ -0,0 +1,36 @@ +passphrase = $passphrase; + $this->contents = $contents; + } + + /** @param non-empty-string $contents */ + public static function plainText(string $contents, string $passphrase = ''): self + { + return new self($contents, $passphrase); + } + + public function contents(): string + { + return $this->contents; + } + + public function passphrase(): string + { + return $this->passphrase; + } +} diff --git a/src/Apple/Provider.php b/src/Apple/Provider.php index ff8c6b940..3aa8dc1b8 100644 --- a/src/Apple/Provider.php +++ b/src/Apple/Provider.php @@ -11,7 +11,6 @@ use Laravel\Socialite\Two\InvalidStateException; use Lcobucci\Clock\SystemClock; use Lcobucci\JWT\Configuration; -use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Validation\Constraint\IssuedBy; use Lcobucci\JWT\Validation\Constraint\LooseValidAt; @@ -147,7 +146,7 @@ public static function verify($jwt) { $jwtContainer = Configuration::forSymmetricSigner( new AppleSignerNone(), - InMemory::plainText('') + AppleSignerInMemory::plainText('') ); $token = $jwtContainer->parser()->parse($jwt); @@ -163,7 +162,7 @@ public static function verify($jwt) if (isset($publicKeys[$kid])) { $publicKey = openssl_pkey_get_details($publicKeys[$kid]->getKeyMaterial()); $constraints = [ - new SignedWith(new Sha256(), InMemory::plainText($publicKey['key'])), + new SignedWith(new Sha256(), AppleSignerInMemory::plainText($publicKey['key'])), new IssuedBy(self::URL), new LooseValidAt(SystemClock::fromSystemTimezone()), ]; From 372d8d1d8acb805828a85a85706af61ff5fb5479 Mon Sep 17 00:00:00 2001 From: Jan Hamrak Date: Sat, 13 May 2023 18:15:40 +0200 Subject: [PATCH 5/7] Update README.md --- src/Apple/README.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Apple/README.md b/src/Apple/README.md index d01f1b05b..4d0035da7 100644 --- a/src/Apple/README.md +++ b/src/Apple/README.md @@ -18,27 +18,28 @@ Please see the [Base Installation Guide](https://socialiteproviders.com/usage/), ], ``` -If using a private key file, add lines to the configuration as follows: +See [Configure Apple ID Authentication](https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple) + +> Note: the client secret used for "Sign In with Apple" is a JWT token that can have a maximum lifetime of 6 months. The article above explains how to generate the client secret on demand and you'll need to update this every 6 months. To generate the client secret for each request, see [Generating A Client Secret For Sign In With Apple On Each Request](https://bannister.me/blog/generating-a-client-secret-for-sign-in-with-apple-on-each-request) + +If you don't have secret token, or you don't want to it do manually, you can use a private key ([see official docs](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048)). +Add lines to the configuration as follows: ```php 'apple' => [ - 'client_id' => env('APPLE_CLIENT_ID'), - 'client_secret' => env('APPLE_CLIENT_SECRET'), // Empty if using private key. - 'key_id' => env('APPLE_KEY_ID'), - 'team_id' => env('APPLE_TEAM_ID'), - 'private_key' => env('APPLE_PRIVATE_KEY'), // Must be absolute path, e.g. /var/www/cert/AuthKey_XYZ.p8 - 'passphrase' => env('APPLE_PASSPHRASE'), // Set if your key have a passphrase. - 'signer' => env('APPLE_SIGNER'), // Signer used for Configuration::forSymmetricSigner(). - 'redirect' => env('APPLE_REDIRECT_URI') + 'client_id' => env('APPLE_CLIENT_ID'), // Required. Bundle ID from Identifier in Apple Developer. + 'client_secret' => env('APPLE_CLIENT_SECRET'), // Empty. We create it from private key. + 'key_id' => env('APPLE_KEY_ID'), // Required. Key ID from Keys in Apple Developer. + 'team_id' => env('APPLE_TEAM_ID'), // Required. App ID Prefix from Identifier in Apple Developer. + 'private_key' => env('APPLE_PRIVATE_KEY'), // Required. Must be absolute path, e.g. /var/www/cert/AuthKey_XYZ.p8 + 'passphrase' => env('APPLE_PASSPHRASE'), // Optional. Set if your private key have a passphrase. + 'signer' => env('APPLE_SIGNER'), // Optional. Signer used for Configuration::forSymmetricSigner(). Default: \Lcobucci\JWT\Signer\Ecdsa\Sha256 + 'redirect' => env('APPLE_REDIRECT_URI') // Required. ], ``` -If you receive error `400 Bad Request {"error":"invalid_client"}` , a possible solution is to use another signer, e.g. "\Lcobucci\JWT\Signer\Ecdsa\Sha256". - +If you receive error `400 Bad Request {"error":"invalid_client"}` , a possible solution is to use another Signer (Asymmetric algorithms), see [Asymmetric algorithms](https://lcobucci-jwt.readthedocs.io/en/stable/supported-algorithms/#asymmetric-algorithms). -See [Configure Apple ID Authentication](https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple) - -> Note: the client secret used for "Sign In with Apple" is a JWT token that can have a maximum lifetime of 6 months. The article above explains how to generate the client secret on demand and you'll need to update this every 6 months. To generate the client secret for each request, see [Generating A Client Secret For Sign In With Apple On Each Request](https://bannister.me/blog/generating-a-client-secret-for-sign-in-with-apple-on-each-request) ### Add provider event listener From 0618773b3a8ad06abe1e37d6250cedc5c704bcf0 Mon Sep 17 00:00:00 2001 From: Jan Hamrak Date: Sun, 24 Sep 2023 20:30:43 +0200 Subject: [PATCH 6/7] Apple - Added support for plain text private keys [pricop] --- src/Apple/Provider.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Apple/Provider.php b/src/Apple/Provider.php index 6bf9f18c7..a0d1bd506 100644 --- a/src/Apple/Provider.php +++ b/src/Apple/Provider.php @@ -141,6 +141,8 @@ protected function getJwtConfig() if (empty($signer) || !class_exists($signer)) { $signer = !empty($private_key_path) ? \Lcobucci\JWT\Signer\Ecdsa\Sha256::class : AppleSignerNone::class; } + + $this->privateKey = $private_key_path; // Support for plain text private keys if (!empty($private_key_path) && file_exists($private_key_path)) { $this->privateKey = file_get_contents($private_key_path); From add78fdbf4f6e3b3aebff96665e50fef07119cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ja=CC=81n=20Hamra=CC=81k?= Date: Sun, 11 Feb 2024 12:43:37 +0100 Subject: [PATCH 7/7] Define additionalConfigKeys() --- src/Apple/Provider.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Apple/Provider.php b/src/Apple/Provider.php index fdccb2500..7895484a7 100644 --- a/src/Apple/Provider.php +++ b/src/Apple/Provider.php @@ -134,18 +134,18 @@ protected function getClientSecret() protected function getJwtConfig() { if (!$this->jwtConfig) { - $private_key_path = config('services.apple.private_key', ''); - $private_key_passphrase = config('services.apple.passphrase', ''); - $signer = config('services.apple.signer', ''); + $private_key_path = $this->getConfig('private_key', ''); + $private_key_passphrase = $this->getConfig('passphrase', ''); + $signer = $this->getConfig('signer', ''); if (empty($signer) || !class_exists($signer)) { $signer = !empty($private_key_path) ? \Lcobucci\JWT\Signer\Ecdsa\Sha256::class : AppleSignerNone::class; } - - $this->privateKey = $private_key_path; // Support for plain text private keys if (!empty($private_key_path) && file_exists($private_key_path)) { $this->privateKey = file_get_contents($private_key_path); + } else { + $this->privateKey = $private_key_path; // Support for plain text private keys } $this->jwtConfig = Configuration::forSymmetricSigner( @@ -368,4 +368,12 @@ public function refreshToken($refreshToken): ResponseInterface ], ]); } + + /** + * {@inheritdoc} + */ + public static function additionalConfigKeys() + { + return ['private_key', 'passphrase', 'signer']; + } }