Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Php Jose Verifier Laravel Package

facile-it/php-jose-verifier

A lightweight PHP library for validating JWT tokens from OpenID and OAuth2 providers. It builds verifiers and decrypters automatically from issuer and client metadata, fetching public keys from a jwks_uri endpoint, with optional GMP support for better performance.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require facile-it/php-jose-verifier

Add ext-gmp to your composer.json require block and to your Docker/Forge/Envoyer build image. The README calls it out explicitly: RSA signature verification falls back to a material slower path without it, and this is the kind of thing you notice only at p95 under load.

Where to look first

  • Facile\JoseVerifier\Builder\* — everything you actually use lives here (AccessTokenVerifierBuilder, IdTokenVerifierBuilder, UserInfoVerifierBuilder). The rest of the package is @internal / implementation detail.
  • Facile\JoseVerifier\Exception\InvalidTokenExceptionInterface — the one exception type you catch.
  • The changelog, before every upgrade. The 0.4 → 0.5 line moved builder namespaces and swapped web-token/* for web-token/jwt-library; the 1.0 line jumps that library to 4.x. Under 0.x semver, assume a minor bump can break you.

First use case: validate an access token from an external IdP

This is the hole in the Laravel ecosystem the package fills — you are the resource server, Keycloak/Auth0/Okta/Entra ID issues the tokens, and the app has never seen the signing key. A tinker smoke test proves the wiring in about five minutes:

use Facile\JoseVerifier\Builder\AccessTokenVerifierBuilder;
use Facile\JoseVerifier\Exception\InvalidTokenExceptionInterface;

$issuer = [
    'issuer'   => 'https://keycloak.example.com/realms/acme',
    'jwks_uri' => 'https://keycloak.example.com/realms/acme/protocol/openid-connect/certs',
];

$client = ['client_id' => 'my-laravel-app'];

$verifier = AccessTokenVerifierBuilder::create($issuer, $client)->build();

try {
    $payload = $verifier->verify($jwtFromAuthorizationHeader);
} catch (InvalidTokenExceptionInterface $e) {
    report($e);
}

$payload is the decoded claims array (sub, aud, exp, scope, custom roles) — that's what you feed into your authorization logic.

Do not reach for this package if you are issuing your own tokens. Passport and tymon/jwt-auth own that. This is strictly the "trust someone else's IdP" piece.

Implementation Patterns

Config + service provider, per issuer

Builders are immutable and cheap to configure, so keep the metadata in config/oidc.php and bind one verifier singleton per named issuer. Multi-tenant (one Laravel app, several IdPs) works naturally this way — key the container binding by tenant:

// config/oidc.php
return [
    'default' => env('OIDC_ISSUER', 'acme'),
    'issuers' => [
        'acme' => [
            'issuer'   => env('OIDC_ISSUER_URL'),
            'jwks_uri' => env('OIDC_JWKS_URI'),
            'client'   => [
                'client_id'     => env('OIDC_CLIENT_ID'),
                'client_secret' => env('OIDC_CLIENT_SECRET'),
            ],
            'cache_ttl' => 86400,
        ],
    ],
];
// AppServiceProvider::register()
$this->app->singleton(AccessTokenVerifierBuilder::class, function ($app) {
    $cfg = config('oidc.issuers.' . config('oidc.default'));

    $jwks = (new JwksProviderBuilder())
        ->withCache($app->make(Psr\SimpleCache\CacheInterface::class))
        ->withCacheTtl($cfg['cache_ttl']);

    return AccessTokenVerifierBuilder::create(
        ['issuer' => $cfg['issuer'], 'jwks_uri' => $cfg['jwks_uri']],
        $cfg['client']
    )->withJwksProviderBuilder($jwks);
});

Build the verifier once per request (or once per Octane worker — it's immutable and safe to hold).

Middleware guard

The idiomatic Laravel shape is a middleware that verifies the bearer token and stashes claims on the request, rather than fighting Auth::guard():

public function handle($request, Closure $next)
{
    $jwt = $request->bearerToken();
    abort_unless($jwt, 401);

    try {
        $claims = app(AccessTokenVerifierBuilder::class)->build()->verify($jwt);
    } catch (InvalidTokenExceptionInterface $e) {
        abort(401, 'Invalid token');
    }

    $request->attributes->set('oidc.claims', $claims);
    // or: Auth::setUser(new OidcUser($claims));

    return $next($request);
}

OIDC login flow: id_token

IdTokenVerifier is the relying-party half of the authorization code flow. Note the extra immutable with* calls — they aren't optional decoration, they enable the at_hash / c_hash / s_hash checks:

$verifier = IdTokenVerifierBuilder::create($issuer, $client)
    ->build()
    ->withState($state)                 // enables s_hash validation
    ->withAccessToken($accessToken)     // enables at_hash validation
    ->withCode($code);                  // enables c_hash validation

$claims = $verifier->verify($idToken);

Skipping withAccessToken() means at_hash is silently unchecked, not rejected. Pass everything you have.

UserInfo verifier

When the userinfo endpoint returns a signed/encrypted JWT body instead of plain JSON, UserInfoVerifierBuilder decrypts and validates in one shot. Same builder signature, same exception type — worth a separate binding if you use it, since the client metadata response algs differ (userinfo_signed_response_alg vs id_token_signed_response_alg).

Bridging Laravel's cache and HTTP client

The package speaks PSR-16 and PSR-18; Laravel speaks its own. Two small bindings buy you the whole integration:

  • PSR-16 over Laravel Cache — either symfony/cache's Psr16Cache wrapping a Laravel store adapter, or ~40 lines of your own CacheInterface implementing get/set/delete/has against Cache::store(). Without it, every request hits jwks_uri.
  • PSR-18 client — bind Guzzle. You won't get Laravel's Http facade middleware (retry, telemetry logging) for free; wrap Guzzle with a retry middleware if you care.

Gotchas and Tips

The PSR-16 cache mismatch is the #1 operational trap. Illuminate\Contracts\Cache\Repository does not implement Psr\SimpleCache\CacheInterface. Wire the adapter, or your JWKS endpoint becomes a hard dependency of every authenticated request — a latency tax and a single point of failure.

JWKS caching has a rotation window. withCacheTtl(86400) is the default and means a rotated signing key isn't seen for up to a day. Either shorten the TTL or handle verification failure by busting the cache and retrying once before you 401. Cheap insurance against a mysterious outage the day your IdP rotates keys.

Catch InvalidTokenExceptionInterface, not a concrete class. It's an interface with multiple implementations behind it (expired, bad signature, bad audience, malformed). If you want to distinguish 401-vs-400 or log a meaningful message, inspect the concrete type or the message — but always catch the interface as a floor.

Test clock injection. Since 0.4.4 the library takes a Psr\Clock\ClockInterface, so you can freeze time in tests instead of sleeping past exp. Bind a fake clock in your test container; verifying exp/nbf behaviour otherwise means real sleep() calls in CI.

auth_time is required when require_auth_time is true (fixed in 0.5.0). If you consume IdP metadata that sets it and your IdP omits the claim, verification fails — and that's correct behaviour, not a bug to work around.

at_hash/c_hash are only checked when you supply the corresponding values (0.4.5 deliberately stopped requiring them). This is the single most common silent-security-gap in integrations: the verifier passes, but token substitution was never actually blocked. Make withState()/withAccessToken()/withCode() unconditional in your flow code.

Pin the version exactly. "facile-it/php-jose-verifier": "1.0.*" (or 0.5.* if you're still on the old line), not ^. Minor bumps have been breaking in practice, and 1.0.0 requires PHP 8.2+ and web-token/jwt-library 4.x — treat upgrades as change-managed work with a test suite that exercises a real token.

Watch the transitive crypto tree, not just this package. Real CVEs will land in web-token/jwt-library and its dependencies. Your composer audit in CI needs to cover the subtree; a green top-level audit means nothing here.

No revocation, no introspection, no opaque tokens. The library verifies self-contained JWTs only. Anything needing immediate invalidation requires short token lifetimes, a denylist keyed on jti you maintain yourself, or a token-introspection call you write by hand. Don't assume verify() protects you against a compromised-but-unexpired token.

Bus factor 1. Five stars, zero dependents, single-vendor stewardship (Facile.it). It's well-built and fills a real gap, but budget for the "we fork or vendor this" scenario in your risk register, and keep your integration surface thin — one service provider, one middleware, one adapter — so a swap to firebase/php-jwt or a hand-rolled web-token integration stays a contained change.

Extension point: if you need behaviour the builders don't expose (custom claim checkers, non-standard validation), pass a configured JwksProviderBuilder and compose around the verifier rather than subclassing — the internals are @internal and will move.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator