jumbojett/openid-connect-php
A lightweight PHP client for OpenID Connect. It supports authorization code flow, token exchange, userinfo retrieval, ID token validation via JWKS, and logout, making it easy to add secure OIDC login to PHP applications.
Install with Composer — there is no Laravel service provider, facade, or config file. You get a single class, Jumbojett\OpenIDConnectClient, and wire it up yourself:
composer require jumbojett/openid-connect-php
Put the IdP settings in config/services.php so they are cacheable and env-driven:
'oidc' => [
'provider' => env('OIDC_PROVIDER_URL'), // e.g. https://login.microsoftonline.com/{tenant}/v2.0
'client_id' => env('OIDC_CLIENT_ID'),
'client_secret' => env('OIDC_CLIENT_SECRET'),
'redirect' => env('OIDC_REDIRECT_URI'), // must match exactly at the IdP
'scopes' => ['openid', 'email', 'profile'],
],
Where to look first: the README's authenticate() example is basically the whole library. Then read the class source for authenticate(), requestTokens(), verifyJWT(), and getUserInfo() — that is the full surface area you will use. Also open your IdP's /.well-known/openid-configuration by hand; almost every problem is a mismatch between what that document says and what you configured.
First use case — the canonical two-route dance:
// routes/web.php
Route::get('/auth/redirect', [OidcController::class, 'redirect']);
Route::get('/auth/callback', [OidcController::class, 'callback']);
public function redirect()
{
$this->client()->authenticate(); // sends 302 to the IdP
}
public function callback(Request $request)
{
$oidc = $this->client();
$oidc->authenticate(); // exchanges ?code= for tokens
$claims = $oidc->getVerifiedClaims(); // or $oidc->getUserInfo()
$user = User::updateOrCreate(
['oidc_sub' => $claims->sub],
['name' => $claims->name ?? $claims->email, 'email' => $claims->email]
);
Auth::login($user);
return redirect('/dashboard');
}
Factory + container binding. Build the client in a small service class rather than in controllers; it keeps secret handling in one place and makes the client mockable in tests. Register it as a singleton only if you do one login per request — otherwise bind it fresh, because the object accumulates state (tokens, verified claims, code verifier).
protected function client(): OpenIDConnectClient
{
$cfg = config('services.oidc');
$oidc = new OpenIDConnectClient($cfg['provider'], $cfg['client_id'], $cfg['client_secret']);
$oidc->setRedirectURL($cfg['redirect']);
$oidc->setCodeChallengeMethod('S256'); // PKCE; recommended even for confidential clients
foreach ($cfg['scopes'] as $s) { $oidc->addScope($s); }
$oidc->setTimeout(10);
return $oidc;
}
Cache discovery and JWKS. The client fetches the well-known document (and the JWKS on verification) on each request. Front it with Cache::remember('oidc.well-known.'.$provider, 3600, ...) by fetching discovery yourself, or accept the latency but be aware that an unreachable IdP becomes an outage for your login route. At minimum, set a timeout so a hung IdP doesn't tie up PHP-FPM workers.
Override endpoints when discovery lies. Azure AD (multi-tenant {tenantid} issuers), Keycloak behind a proxy, and some Auth0 setups publish endpoints with the wrong host. Set the issuer or individual endpoints explicitly instead of trusting iss from discovery.
Refresh is manual. Keep refresh_token in your own store, call the refresh path on the client, and handle invalid_grant by forcing re-login. Do not assume the library persists tokens for you.
Logout is DIY. Discovery exposes end_session_endpoint, but there is no helper. Build the RP-initiated logout URL yourself with id_token_hint and post_logout_redirect_uri, then clear the Laravel session.
Claims → user mapping. Key users off (iss, sub), not email — email can change, and multi-tenant IdPs reuse it. Validate aud/iss (the library does, but confirm your setIssuer override is correct), and keep a local users record rather than relying on IdP roles for authorization.
Testing. Point the provider URL at a local stub or use Http::fake() around a thin wrapper; asserting on the library internals is painful. Test your controller by injecting a fake client that returns fixed claims.
$_SESSION is the big one. The library stores state, nonce, and the PKCE verifier in the PHP superglobal $_SESSION. Laravel's session store is separate; if no native session is active, state validation fails with confusing "state does not match" errors or silently restarts the flow. Either start a native session on those two routes or manage state yourself and verify it in the callback.authenticate() may exit. On the redirect leg it emits a Location header and terminates. Don't call it inside an output buffer or code that expects a return value; keep it at the top of a dedicated controller action.http vs https, port, and query string all matter to the IdP. Behind a load balancer, force https and generate the redirect from config('app.url'), not request()->fullUrl().iss that differs from the discovery URL. Override with setIssuer() or you get "invalid issuer" after a successful login.client_secret_post; the client defaults toward HTTP Basic. If the token exchange 401s with a valid secret, that is the cause — check the version's handling and patch or switch provider config.setVerifyPeer(false) / setVerifyHost(false). They are for local self-signed IdPs only. Set a CA bundle path instead.iat/exp validation fails if the server clock drifts. Run NTP; there is no reliable leeway knob in every version.openid is added automatically; email and profile are not. Missing profile is the usual cause of empty name claims.Authorization: Bearer header rather than a query parameter, and a non-JSON error page throws an opaque exception. Wrap in try/catch (OpenIDConnectClientException $e) and log the message.How can I help you explore Laravel packages today?