jeremy379/laravel-openid-connect
A Laravel package for adding OpenID Connect authentication to your app. Let users sign in through any compliant OIDC provider with a simple configuration-driven flow covering authorization, token exchange, and user info retrieval.
Install and wire up
composer require jeremy379/laravel-openid-connect
config/services.php — this is the first place to look, it's where all package configuration lives:'oidc' => [
'client_id' => env('OIDC_CLIENT_ID'),
'client_secret' => env('OIDC_CLIENT_SECRET'),
'redirect' => env('OIDC_REDIRECT_URI'), // must match the IdP registration exactly
'base_url' => env('OIDC_BASE_URL'), // e.g. https://idp.example.com
'scopes' => ['openid', 'profile', 'email'],
],
oidc).Route::get('/auth/redirect', fn () => Socialite::driver('oidc')->redirect());
Route::get('/auth/callback', function () {
$user = Socialite::driver('oidc')->user();
// $user->id (the `sub` claim), $user->name, $user->email, $user->token
});
First use case: "log in with our corporate IdP" — one OIDC provider, session-based login, sub claim mapped to a local users row. Everything else (discovery document fetch, JWKS retrieval, id_token signature validation, token exchange) is handled for you.
Version note: 3.3.0 is a maintenance release — PHP 8.5 CI coverage plus removal of the deprecated ReflectionMethod::setAccessible() call. No API changes, no config changes. It's a safe drop-in from 3.2.x and the first version to be clean on PHP 8.5.
Persisting the identity. The sub claim is the only stable identifier — store it in its own indexed column (oidc_sub) and use it for lookup, then treat email/name as refreshable attributes. Emails get reassigned; sub values don't.
$user = User::updateOrCreate(
['oidc_sub' => $oidcUser->getId()],
['name' => $oidcUser->getName(), 'email' => $oidcUser->getEmail()]
]);
Auth::login($user);
Keeping the raw tokens. $user->token gives you the access token and the raw response is on $user->user / the access token response. Persist the refresh_token if you need to call downstream APIs after login, and store expires_in alongside it rather than assuming a lifetime.
RP-initiated logout. Pair the login flow with the package's logout support so the IdP session is actually terminated rather than just your Laravel session:
return redirect(Socialite::driver('oidc')->logout($idToken, route('home')));
Keep the id_token from login in the session for the id_token_hint — without it, many IdPs either refuse the logout or prompt the user.
Multiple providers / tenants. Register several keys in config/services.php (oidc, oidc-partner, …) and resolve the driver name from the request (subdomain, route prefix, or an auth_providers table). This is cheaper than instantiating the provider manually and keeps discovery/JWKS caching per-provider.
Testing. Fake the discovery and token endpoints with Http::fake() in feature tests rather than mocking the provider — that exercises the real token validation path. For unit-level controller tests, Socialite::shouldReceive('driver->user') and return a stub with getId()/getEmail(). Include one test with a genuinely expired/forged id_token so you notice if signature verification ever silently degrades.
Route placement. Put the callback behind the web middleware group — Socialite relies on the session for state and nonce correlation. API-only apps should either add a lightweight session or handle the flow in a dedicated stateful route group.
redirect must be byte-identical to what's registered at the IdP — scheme, host, trailing slash, port. This is the single most common "it works locally, fails in staging" cause.state/nonce mismatches throw Laravel\Socialite\Two\InvalidStateException. In practice this means the session was lost between redirect and callback: wrong middleware group, SESSION_DRIVER=array, a load balancer without sticky sessions, or a user who sat on the login page until the session expired.base_url, clear the cache or you'll chase phantom signature failures. Conversely, don't disable caching in production — you'll add a round trip to every request in the flow.openid. Without it you get a plain OAuth2 response, no id_token, and user() returns a half-populated object. profile and email are what actually populate name/email.id_token. If user() returns empty name/email, the provider isn't exposing a userinfo endpoint or the claims you asked for aren't in the granted scopes — check the raw response before assuming a package bug.iat/exp validation failures on otherwise-valid tokens. If you control the IdP, keep hosts on NTP; if not, be aware there's limited tolerance and near-zero drift budgets on some providers.client_id, missing client_secret for confidential clients, mismatched redirect, unregistered scope).Socialite::extend('oidc', …) and the standard Socialite custom-provider patterns still apply if you need to override scope handling or claim mapping rather than forking the package.ReflectionMethod::setAccessible() call that emitted deprecation notices, so if you're testing against 8.5 you can drop any error_reporting suppression you added around the package.How can I help you explore Laravel packages today?