league/openid-connect-claims
Provides strongly-typed PHP value objects for OpenID Connect claims, making it easy to build, validate, and serialize standard and custom claims for OIDC ID tokens and userinfo responses. Part of the League of Extraordinary Packages.
The package is small and purpose-built: it gives you a typed, object-oriented view over the claim set inside an OpenID Connect ID token. It is not a JWT decoder, a token verifier, or an OAuth2 client — it only models the claims payload.
Minimal setup:
composer require league/openid-connect-claims
Where to look first:
README.md — confirm the current class name (src/ is the source of truth; the primary type is an ID-claims-set object that wraps an associative array of decoded claims).src/ — read the main claims-set class and any interface it implements. The public surface is mostly immutable getters plus an array/toArray() style accessor.composer.json — note the PHP constraint and the absence of dependencies. It is a standalone value object, so it drops into any stack without pulling in PSR-7, JWT libraries, etc.First use case — take the payload you already obtained from your OAuth2 client, wrap it, and read standard claims:
use League\OpenIDConnectClaims\IdClaimsSet;
// $payload is the already-decoded JSON body of the ID token
// (signature verification must have happened elsewhere).
$claims = new IdClaimsSet($payload);
$subject = $claims->getSubject(); // "sub"
$issuer = $claims->getIssuer(); // "iss"
$audience = $claims->getAudience(); // "aud"
$email = $claims->getClaim('email'); // non-modelled / optional claims
Treat the object as the boundary between “raw array coming off the wire” and “typed claims your application code consumes.” That single conversion is the package’s whole reason to exist.
Wrap, don’t leak, the raw array. The most common day-to-day pattern is to construct the claims set once — right after your OAuth2/OIDC client exchanges the code — and pass the object (or a domain DTO built from it) through the rest of the request. Controllers, middleware, and services should not be reading $payload['sub'] directly.
Custom provider integration. With league/oauth2-client, subclass or decorate your provider so getResourceOwner()/your own accessor returns an IdClaimsSet:
class MyIdpProvider extends AbstractProvider
{
public function getIdClaims(AccessToken $token): IdClaimsSet
{
$payload = $this->decodeIdToken($token); // your JWT verify + decode
return new IdClaimsSet($payload);
}
}
Keep verification separate and explicit. The claim set is a passive model. Your pipeline should be:
IdClaimsSet.iss, aud, exp, nonce against expectations.Steps 2 and 5 are your responsibility — do not assume the package performs them.
Validation helper/service. Since the object is immutable and cheap, make a small IdClaimsValidator that takes an IdClaimsSet plus expected issuer/audience/nonce and returns a boolean or throws. This keeps policy out of the value object and makes the rules testable:
final class IdClaimsValidator
{
public function __construct(
private string $expectedIssuer,
private string $expectedAudience,
) {}
public function assertValid(IdClaimsSet $claims): void
{
if ($claims->getIssuer() !== $this->expectedIssuer) {
throw new InvalidIdToken('issuer mismatch');
}
if (!in_array($this->expectedAudience, (array) $claims->getAudience(), true)) {
throw new InvalidIdToken('audience mismatch');
}
if ($claims->getExpirationTime() <= time()) {
throw new InvalidIdToken('token expired');
}
}
}
Session / cache pattern. Because the object is a simple wrapper, serialize it as the raw array (toArray() / getClaims()) when persisting to session or cache, and reconstruct on read. Avoid serializing the object itself — that couples your storage format to an unmaintained class.
Extension via composition. If you need convenience getters the package lacks (e.g. getName(), getEmail()), add them on your own DTO or a thin wrapper rather than subclassing the claims set. Subclassing buys you nothing here and makes upgrades/migrations harder.
Testing pattern. Since the constructor takes a plain array, tests are trivial: build an IdClaimsSet from a fixture array and assert against your domain-building logic. No external services, no mocking of the package needed.
IdClaimsSet says nothing about whether the token was signed by the IdP. Parse and verify elsewhere (e.g. firebase/php-jwt, web-token/jwt-framework) before constructing the claims set.getExpirationTime() throws on expiry or that issuer/audience checks happen automatically. Do them yourself, every time.aud may be a string or an array of strings. Normalize (array) $claims->getAudience() before comparing — a strict === against a string will bite you on multi-audience tokens.exp, iat, auth_time) are seconds-since-epoch numeric values. If the package returns them as-is, do not compare against DateTime objects; convert explicitly. Watch for integer/string coercion if the payload came through json_decode with associative arrays that were re-encoded.sub, aud, auth_time). A generic getClaim('Auth_Time') will quietly miss. Stick to the spec names.email, name, picture, etc. are only present if the IdP was asked for them via scopes. Always null-check; don’t let a missing email blow up your user-mapping code.$claims->toArray() (or getClaims()) rather than individual getters — the raw array exposes typos and type coercions that the typed getters hide.How can I help you explore Laravel packages today?