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 Openid Client Laravel Package

facile-it/php-openid-client

Full OpenID Connect client for PHP supporting OAuth 2.0 authorization code, implicit and hybrid flows, discovery, dynamic client registration, UserInfo/ID tokens with JWT signing and encryption, refresh tokens, and token revocation/introspection.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install with a PSR-18 stack. The package only declares PSR interfaces, so you must supply concrete implementations. In a Laravel app Guzzle + guzzlehttp/psr7 is already present:

composer require facile-it/php-openid-client guzzlehttp/psr7
php -m | grep gmp   # gmp may be required for RSA/JWK handling

If you want the optional session middleware you also need dflydev/fig-cookies. If you need non-RSA signing/encryption algorithms, install the corresponding web-token/jwt-library algorithm packages — only RSA ships by default.

2. Bind the three moving parts in a service provider. Day-to-day you interact with an Issuer (discovery document + JWKS), a Client (your metadata), and stateless services (AuthorizationService, UserInfoService, …). Build them once and resolve from the container:

$this->app->singleton(Issuer::class, fn () => (new IssuerBuilder())
    ->setMetadataProviderBuilder(
        (new MetadataProviderBuilder())->setCache(cache()->store())->setCacheTtl(86400)
    )
    ->setJwksProviderBuilder(
        (new JwksProviderBuilder())->setCache(cache()->store())->setCacheTtl(3600)
    )
    ->build(config('openid.discovery_url')));

$this->app->singleton(ClientInterface::class, fn ($app) => (new ClientBuilder())
    ->setIssuer($app->make(Issuer::class))
    ->setClientMetadata(ClientMetadata::fromArray([
        'client_id'     => config('openid.client_id'),
        'client_secret' => config('openid.client_secret'),
        'token_endpoint_auth_method' => 'client_secret_basic',
        'redirect_uris' => [route('openid.callback')],
    ]))
    ->build());

$this->app->singleton(AuthorizationService::class, fn () => (new AuthorizationServiceBuilder())->build());
$this->app->singleton(UserInfoService::class, fn () => (new UserInfoServiceBuilder())->build());

3. First use case — the login redirect. Two routes and nothing else:

// GET /login  -> RedirectController
$state = Str::random(40);
$nonce = Str::random(40);
session(['openid_state' => $state, 'openid_nonce' => $nonce]);

return redirect($authorizationService->getAuthorizationUri($client, [
    'scope' => 'openid profile email offline_access',
    'state' => $state,
    'nonce' => $nonce,
]));

Where to look first: the README's "Basic Usage" block is the entire happy path; after that, the Service\Builder\* classes are the real API surface (each service has a builder with setters for the PSR-18 client, request factory, and options), and Facile\OpenIDClient\Token\TokenSetInterface is what you'll be passing around the most.

Implementation Patterns

Callback handling belongs in a controller, not middleware. The package ships PSR-15 middlewares (ClientProviderMiddleware, AuthRequestProviderMiddleware, AuthRedirectHandler, CallbackMiddleware, UserInfoMiddleware), but Laravel's HTTP kernel isn't PSR-15 and the session plumbing (SessionCookieMiddleware writing a fig-cookie) duplicates Laravel's own session. In practice you translate the Laravel request to PSR-7 and call the service directly:

use Symfony\Bridge\PsrHttpMessage\Factory\Psr17Factory; // or nyholm/psr7 + manual copy

$psrRequest = app(Psr17Factory::class)->createRequest($request);
$params = $authorizationService->getCallbackParams($psrRequest, $client);

abort_unless(hash_equals(session()->pull('openid_state'), $params['state'] ?? ''), 400, 'State mismatch');

$tokenSet = $authorizationService->callback($client, $params);
$claims   = $tokenSet->claims();   // verifies signature/iss/aud/exp on the ID token

abort_unless(hash_equals(session()->pull('openid_nonce'), $claims['nonce'] ?? ''), 400, 'Nonce mismatch');

Auth::login(User::firstOrCreate(['sub' => $claims['sub']], $claims), true);
session(['openid.refresh_token' => $tokenSet->getRefreshToken()]);

callback() exchanges the code and validates the ID token, but state and nonce are yours to check — the middleware does it for you only if you adopt SessionCookieMiddleware.

Resolve the local user via sub, not email. Issuers change email frequently; sub is the stable identifier. Store sub + issuer (for multi-provider apps) as a unique compound key.

Refresh deliberately. $authorizationService->refresh($client, $refreshToken) returns a new TokenSet. Persist the new refresh token every time — many IdPs rotate it and the old one dies. Guard with a try/catch: an expired/revoked refresh token raises an exception and should mean "log the user out", not a 500:

try {
    $tokenSet = $authorizationService->refresh($client, $stored);
    session(['openid.refresh_token' => $tokenSet->getRefreshToken()]);
} catch (\Throwable) {
    Auth::logout();
    session()->invalidate();
    return redirect()->route('login');
}

Separate the "who is this" call from the "tell me about them" call. $tokenSet->claims() gives you the ID token claims (cheap, always present). $userInfoService->getUserInfo($client, $tokenSet) makes a network call to the UserInfo endpoint for the richer profile — only call it when you actually need profile fields, and cache the result keyed on sub + updated_at claim if you render it on every page.

For machine-to-machine, skip the browser dance entirely and use the client-credentials grant via AuthorizationService (or IntrospectionService/RevocationService for token hygiene). This is the easiest place to start if you're not sure the package fits — no redirects, no cookies.

Multi-tenant / multi-IdP: register one named Client per issuer in a config array and resolve by a route segment or a provider column on your users table. Key your PSR-16 cache by issuer URL (the builders use the issuer URI as part of the key) so tenants don't collide.

Testing: the services accept an injectable PSR-18 client via their builders, so bind a mock ClientInterface in tests and assert the authorization URI is well-formed and that callback() receives the right params. You never need a live IdP for the bulk of your tests.

Gotchas and Tips

  • The cache is not optional in production. Without setCache() on MetadataProviderBuilder and JwksProviderBuilder, every single request fetches /.well-known/openid-configuration and the JWKS endpoint. This is the number-one performance and reliability issue with this library. Note the cache type is PSR-16, and Illuminate\Contracts\Cache\Repository extends Psr\SimpleCache\CacheInterface, so cache()->store() works directly — but keys are namespaced by the provider, so don't hand-roll your own keys unless you also add a prefix.
  • gmp is a soft requirement that bites hard. Everything works on dev machines and fails in a slim Docker image when the IdP signs with RS256 and the JWK math needs gmp. Add ext-gmp to your composer.json require block and to your Dockerfile.
  • Converting the Laravel request to PSR-7 is the usual stumbling block. getCallbackParams() requires a ServerRequestInterface. symfony/psr-http-message-bridge (already a transitive dep of Laravel) plus guzzlehttp/psr7 or nyholm/psr7 is the least friction. Watch out that the bridge drops the raw body unless you pass the content through, which matters for POST/form_post response modes.
  • token_endpoint_auth_method defaults matter. client_secret_basic URL-encodes credentials (fixed in 0.1.4) — if you set it to client_secret_post or use private_key_jwt, the IdP must have the client registered accordingly or you get an opaque invalid_client with no useful message.
  • Redirect URIs are matched byte-for-byte, including trailing slashes and http vs https. route('openid.callback') generating http:// behind a proxy is a classic production-only failure — configure TrustProxies/APP_URL first.
  • Scope: openid must be in the scope or you get an OAuth token without an ID token. offline_access is required for a refresh token; asking for it silently yields no refresh token on some IdPs.
  • $tokenSet->getIdToken() can be null (e.g. client-credentials grant, or a provider that doesn't return one). Always branch on it before calling claims().
  • Algorithm coverage: only RSA is bundled. If the provider uses ES256 (common with Azure AD / some Keycloak configs) or you request encrypted ID tokens or a signed request object, you must install the matching JWT Framework algorithm package or you'll get a confusing "algorithm not supported" exception at verification time.
  • Upgrading to 1.x is a breaking change. It drops PHP 8.1 and moves to web-token/jwt-library 4.x, which has its own migration guide — pin and test this in a branch, especially if you also depend on facile-it/jose-verifier (the JWKS provider lives there).
  • Error surfaces are exception-heavy. Facile\OpenIDClient\Exception\* includes runtime, invalid-request, and OAuth-error variants. Catch OAuthException separately so you can surface error_description to logs while showing the user a generic "sign-in failed" page — never leak the raw exception message.
  • No RP-initiated logout helper. If you need end_session_endpoint, pull it from $issuer->getMetadata()->getEndSessionEndpoint() and build the redirect yourself; the library doesn't wrap it.
  • Debugging: attach Guzzle's Middleware::log() (or a tap on the handler stack) to the PSR-18 client you inject into the builders. Nearly every "it just doesn't work" case is a discovery, JWKS, or token-endpoint HTTP call you can't see.
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.
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc