Skip to content

Claim and header validation

After verifying a signature or decrypting a token, you must validate the claims and headers before trusting the token content.

Danger

Never use a token payload without validating its claims. An expired or wrongly-issued token must be rejected.

Validate claims

The ClaimCheckerManager verifies claims contained in the token payload.

<?php

use Jose\Component\Checker\AudienceChecker;
use Jose\Component\Checker\ClaimCheckerManager;
use Jose\Component\Checker\ExpirationTimeChecker;
use Jose\Component\Checker\IssuedAtChecker;
use Jose\Component\Checker\IssuerChecker;
use Jose\Component\Checker\NotBeforeChecker;
use Symfony\Component\Clock\NativeClock;

require_once 'vendor/autoload.php';

// A PSR-20 clock implementation is required for time-based checkers
$clock = new NativeClock();

$claimCheckerManager = new ClaimCheckerManager([
    new IssuedAtChecker($clock),
    new NotBeforeChecker($clock),
    new ExpirationTimeChecker($clock),
    new IssuerChecker(['https://auth.example.com']),
    new AudienceChecker('https://api.example.com'),
]);

// $payload is the decoded payload from a verified JWS or decrypted JWE
$payload = json_decode($jws->getPayload(), true);

// Check claims. The second parameter lists the mandatory claims.
$claimCheckerManager->check($payload, ['iss', 'aud', 'exp']);
// Throws an exception if any check fails

Info

The NativeClock class is from the symfony/clock component. You can use any Psr\Clock\ClockInterface implementation.

composer require symfony/clock


Allow a time drift

Network latency or clock skew between servers may cause valid tokens to be rejected. You can allow a small time drift (in seconds):

$claimCheckerManager = new ClaimCheckerManager([
    new ExpirationTimeChecker($clock, allowedTimeDrift: 10), // 10 seconds tolerance
    new NotBeforeChecker($clock, allowedTimeDrift: 10),
    new IssuedAtChecker($clock, allowedTimeDrift: 10),
]);

Custom claim validation with CallableChecker

For application-specific claims, use the CallableChecker:

<?php

use Jose\Component\Checker\CallableChecker;
use Jose\Component\Checker\ClaimCheckerManager;

// Validate that the "role" claim is one of the allowed values
$roleChecker = new CallableChecker(
    'role',
    static function (mixed $value): bool {
        return in_array($value, ['admin', 'editor', 'viewer'], true);
    }
);

$claimCheckerManager = new ClaimCheckerManager([
    $roleChecker,
    // ... other checkers
]);

$claimCheckerManager->check($payload, ['role']);

Validate with IsEqualChecker

For simple equality checks on claims or headers:

<?php

use Jose\Component\Checker\ClaimCheckerManager;
use Jose\Component\Checker\IsEqualChecker;

// Ensure the "type" claim equals "access_token"
$claimCheckerManager = new ClaimCheckerManager([
    new IsEqualChecker('type', 'access_token'),
]);

$claimCheckerManager->check($payload, ['type']);

Complete example: Sign, verify, and validate

A full example combining token creation, verification, and claim validation:

<?php

use Jose\Component\Checker\AlgorithmChecker;
use Jose\Component\Checker\AudienceChecker;
use Jose\Component\Checker\ClaimCheckerManager;
use Jose\Component\Checker\ExpirationTimeChecker;
use Jose\Component\Checker\HeaderCheckerManager;
use Jose\Component\Checker\IssuedAtChecker;
use Jose\Component\Checker\IssuerChecker;
use Jose\Component\Checker\NotBeforeChecker;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\JWSBuilder;
use Jose\Component\Signature\JWSLoader;
use Jose\Component\Signature\JWSTokenSupport;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Signature\Serializer\CompactSerializer;
use Jose\Component\Signature\Serializer\JWSSerializerManager;
use Symfony\Component\Clock\NativeClock;

require_once 'vendor/autoload.php';

// --- 1. Create and sign the token ---

$jwkFactory = new JWKFactory();
$privateKey = $jwkFactory->ec('P-256', ['alg' => 'ES256', 'use' => 'sig']);

$algorithmManager = new AlgorithmManager([new ES256()]);

$payload = json_encode([
    'iss' => 'https://auth.example.com',
    'aud' => 'https://api.example.com',
    'sub' => 'user-42',
    'iat' => time(),
    'nbf' => time(),
    'exp' => time() + 3600,
]);

$jws = (new JWSBuilder($algorithmManager))
    ->withPayload($payload)
    ->addSignature($privateKey, ['alg' => 'ES256'])
    ->build();

$token = (new CompactSerializer())->serialize($jws);

// --- 2. Load and verify the signature ---

$publicKey = $privateKey->toPublic();

$jwsLoader = new JWSLoader(
    new JWSSerializerManager([new CompactSerializer()]),
    new JWSVerifier($algorithmManager),
    new HeaderCheckerManager(
        [new AlgorithmChecker(['ES256'])],
        [new JWSTokenSupport()]
    )
);

$jws = $jwsLoader->loadAndVerify($token, $publicKey)->getJws();

// --- 3. Validate the claims ---

$clock = new NativeClock();

$claimCheckerManager = new ClaimCheckerManager([
    new IssuedAtChecker($clock),
    new NotBeforeChecker($clock),
    new ExpirationTimeChecker($clock),
    new IssuerChecker(['https://auth.example.com']),
    new AudienceChecker('https://api.example.com'),
]);

$claims = json_decode($jws->getPayload(), true);
$claimCheckerManager->check($claims, ['iss', 'aud', 'sub', 'exp']);

// If no exception is thrown, the token is valid and trusted
echo 'Token is valid for user: ' . $claims['sub'];