Package · jwt

Verifying with JWKS

Identity providers publish their public keys as a JSON Web Key Set (JWKS) at a well-known URL. The JWK parser turns that document into a map of PublicKeys keyed by kid, so you can verify a provider’s tokens — and survive key rotation — without hard-coding anything.

Fetch and parse the key set

JWK::parseKeySet() takes the decoded JWKS array and returns [kid => PublicKey] — exactly the shape decode() wants:

verify-idtoken.php
<?php

use Flytachi\Jwt\JWK;
use Flytachi\Jwt\JWT;

// 1. Fetch the provider's JWKS document
$jwksJson = file_get_contents('https://www.googleapis.com/oauth2/v3/certs');
$jwks = json_decode($jwksJson, true);

// 2. Parse it into [kid => PublicKey]
$publicKeys = JWK::parseKeySet($jwks);

// 3. Verify the ID token — decode() matches the token's kid to a key
$payload = JWT::decode($idToken, $publicKeys);

echo $payload->getClaim('email');

Why kid rotation just works

Providers rotate keys and serve several at once, each with a distinct kid. Because you pass the whole set to decode(), it selects the one the token was signed with. When the provider rotates, you re-fetch the set — no code change.

Cache the key set

Don’t fetch the JWKS on every request — it changes rarely. Cache the parsed keys and refresh periodically (respect the endpoint’s Cache-Control where you can):

php
function getProviderKeys(CacheInterface $cache): array
{
  return $cache->remember('google_jwks', 3600, function () {
      $jwks = json_decode(
          file_get_contents('https://www.googleapis.com/oauth2/v3/certs'),
          true
      );
      return JWK::parseKeySet($jwks); // array of PublicKey, keyed by kid
  });
}

Malformed keys are skipped, not fatal

parseKeySet() skips any individual key it can’t parse (and any entry missing a kid) and returns the rest. A single bad entry in the provider’s document won’t break verification for the good keys.

Parse a single key

If you already know which key you need, parseKey() converts one JWK object into a PublicKey:

php
use Flytachi\Jwt\JWK;

$publicKey = JWK::parseKey([
  'kty' => 'RSA',
  'alg' => 'RS256',
  'n'   => '0vx7agoebGcQ...',
  'e'   => 'AQAB',
]);

Supported key types

The parser handles the three standard JWK key types:

kty Required fields Produces
RSA n, e RSA public key for RS* verification
EC crv, x, y ECDSA public key (P-256/P-384/P-521) for ES*
oct k Symmetric secret for HS* verification

Every JWK must also carry alg (the algorithm) — the parser throws without it, since the resulting PublicKey is pinned to that algorithm.

Always validate the claims too

A valid signature only proves who signed it. For third-party tokens you must also check the token was meant for you and issued by whom you expect — verify iss and aud yourself after decoding:

php
use Flytachi\Jwt\JWTException;

$payload = JWT::decode($idToken, $publicKeys); // signature + exp/nbf/iat

if ($payload->getClaim('iss') !== 'https://accounts.google.com') {
  throw new JWTException('Unexpected issuer.');
}
if ($payload->getClaim('aud') !== 'my-client-id.apps.googleusercontent.com') {
  throw new JWTException('Token was not issued for this app.');
}

$userId = $payload->getClaim('sub');

exp is checked, iss/aud are not

decode() validates the time claims (exp, nbf, iat) automatically, but it does not check iss or aud — those are application-specific. Always assert them yourself when consuming tokens you didn’t issue.