Created
March 5, 2025 15:29
-
-
Save vsg24/0d8384fe5e37c6d82d9555176a6fdff7 to your computer and use it in GitHub Desktop.
Verify Apple Login JWT in Laravel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Actions; | |
use Firebase\JWT\JWK; | |
use Illuminate\Support\Facades\Cache; | |
use Firebase\JWT\JWT; | |
use GuzzleHttp\Client; | |
use Exception; | |
class AppleLogin | |
{ | |
// Apple JWKS endpoint | |
const string APPLE_JWKS_URL = 'https://appleid.apple.com/auth/keys'; | |
/** | |
* Fetch Apple public keys (JWKs). Cache them for performance. | |
*/ | |
public static function fetchAppleJWKs(): array | |
{ | |
$cacheKey = 'apple_jwks'; | |
// Remember for 72 hours (Apple rarely rotates keys) | |
return Cache::remember($cacheKey, 60 * 60 * 72, function () { | |
$client = new Client(); | |
$response = $client->get(self::APPLE_JWKS_URL); | |
$body = $response->getBody()->getContents(); | |
$json = json_decode($body, true); | |
if (json_last_error() !== JSON_ERROR_NONE) { | |
throw new Exception('Unable to parse Apple JWKs'); | |
} | |
return $json ?? []; | |
}); | |
} | |
public static function verifyAppleIdToken(string $idToken, array|string $clientId, ?string $nonce = null): object | |
{ | |
// Extract 'kid' from token header | |
$headerBase64 = explode('.', $idToken)[0]; | |
$header = JWT::jsonDecode(JWT::urlsafeB64Decode($headerBase64)); | |
$kid = $header->kid; | |
// Fetch and parse JWKS | |
$jwks = self::fetchAppleJWKs(); | |
$publicKeys = JWK::parseKeySet($jwks); | |
// Check if key exists for this 'kid' | |
if (!isset($publicKeys[$kid])) { | |
throw new Exception("Unable to find matching key for kid: {$kid}"); | |
} | |
$key = $publicKeys[$kid]; | |
// Decode and verify the token | |
try { | |
$payload = JWT::decode($idToken, $key); | |
} catch (Exception $e) { | |
throw new Exception('Invalid token signature or claims: ' . $e->getMessage()); | |
} | |
// Additional claim verifications | |
if (!isset($payload->iss) || $payload->iss !== 'https://appleid.apple.com') { | |
throw new Exception("Invalid issuer: expected https://appleid.apple.com"); | |
} | |
$audArray = is_array($payload->aud) ? $payload->aud : [$payload->aud]; | |
$clientIds = is_array($clientId) ? $clientId : [$clientId]; | |
// Check for at least one match | |
$matchedAud = array_intersect($audArray, $clientIds); | |
if (empty($matchedAud)) { | |
throw new Exception("Invalid audience: " . implode(',', $audArray)); | |
} | |
// If you used a nonce when making the request, verify it: | |
if ($nonce && (!isset($payload->nonce) || $payload->nonce !== $nonce)) { | |
throw new Exception('Nonce does not match.'); | |
} | |
// Also check expiration time if you want to ensure it’s not expired: | |
if (!isset($payload->exp) || (time() > $payload->exp)) { | |
throw new Exception('Token has expired.'); | |
} | |
return $payload; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment