itk-dev/openid-connect
OpenID Connect authentication for Laravel applications. Handles the authorization code flow, discovery, token exchange and validation, userinfo retrieval, and login/logout routes, with configuration via environment variables so apps can integrate an OIDC identity provider quickly.
1. Install and inspect before configuring
composer require itk-dev/openid-connect
This package has a single star and zero dependents, so the source is the documentation. Before wiring anything up, resolve the version you actually got and read it:
composer show itk-dev/openid-connect
ls vendor/itk-dev/openid-connect/src
Look for three things in that order: the service provider (what it registers and what it publishes), the config file (the authoritative list of env keys — do not trust the README to be current), and the guard/driver class (how it hooks into Auth::guard()).
2. Publish and read the config
php artisan vendor:publish --provider="ItkDev\OpenIDConnect\...ServiceProvider"
Open config/openid-connect.php (or whatever it publishes as) and map each key to an .env entry. Expect the discovery-based shape:
OIDC_DISCOVERY_URL=https://idp.example.com/.well-known/openid-configuration
OIDC_CLIENT_ID=laravel-app
OIDC_CLIENT_SECRET=...
OIDC_REDIRECT_URI=https://app.example.com/auth/callback
OIDC_SCOPES="openid profile email"
Never hardcode the issuer — the whole point of the discovery document is that the authorization, token, userinfo, and JWKS endpoints come from one URL, and the provider can move them without you shipping code.
3. Register the guard
In config/auth.php, add a provider and guard pointing at the package's user provider / guard driver, then protect routes with ->middleware('auth:oidc') or Route::middleware('auth') once the guard is the default.
4. First use case: staff login against the org IdP
The realistic first ship is not "replace all auth" — it's an internal app where the IdP (Keycloak, Entra ID, an itk-dev municipal setup) is the source of truth for staff. Keep the existing users table, add oidc_sub (nullable, unique, indexed), and let the callback resolve-or-create. That gives you a revert path if the package turns out to be a dead end.
5. Smoke-test the round trip
Hit the login route, complete consent at the IdP, and confirm you land back on the callback with a populated Auth::user(). If that works, everything else is claims mapping and edge cases.
Guard-first, not middleware-first. Resist writing your own controller that calls the package's token exchange by hand. Register the guard and let Laravel's auth middleware do the redirect for guests and the identity for everyone else. Your app code should never see a token — it should only see Auth::user().
Resolve by sub, never by email. sub is the only stable OIDC identifier. Emails get reassigned between people at real organisations; if you key users on email you will eventually hand one person's account to another. Store sub at creation time and look it up on every login:
$user = User::firstOrCreate(
['oidc_sub' => $claims['sub']],
['name' => $claims['name'], 'email' => $claims['email']]
);
Treat email, name, and any group/role claims as refreshed on each login, not as immutable.
Isolate claims mapping behind one method. The package maps claims onto your model, but put the translation in a single resolveOidcUser(array $claims): User method (a dedicated action or the user provider). Providers differ wildly in what they emit — Entra ID sends preferred_username where Keycloak sends email, and group claims may arrive as a groups array, a roles array, or a resource_access object. One function, one place to fix it.
Multi-provider via separate guards. Environment-driven config usually means one provider per app instance, but if you need two (e.g. staff vs. external partners), define two guards with distinct client credentials and distinct callback routes rather than branching inside one config. Discovery caching and JWKS are keyed per provider, so shared config is where multi-tenant setups break.
Local fake IdP for tests and offline dev. Run Keycloak or oauth2-proxy/Dex in Docker Compose, point OIDC_DISCOVERY_URL at it, and seed a test realm. This is the single highest-value investment: it lets you exercise the full redirect flow in CI and in Dusk without touching the real IdP, and it lets new team members run the app without an account.
Logout has two halves. Local Auth::logout() plus session invalidation is not SSO logout. If you need RP-initiated logout, read end_session_endpoint from the same discovery document and redirect there with an id_token_hint — or at minimum accept that the user stays signed in at the IdP and a re-login is silent.
Keep a password fallback for a defined window. A local admin account with a password (or an artisan command that grants a session) has saved more than one deployment where the IdP was misconfigured. Gate it behind an env flag and remove it once you trust the flow.
The redirect URI is an exact string match. No trailing slash, correct scheme, correct host including port. https://app.test/auth/callback and https://app.test/auth/callback/ are different URIs to the IdP. The most common failure is a mismatch between the value in .env and the value registered in the IdP console, often caused by APP_URL being wrong behind a proxy.
Behind a load balancer, Laravel generates the wrong callback URL. Configure TrustProxies properly or URLs come out as http:// and internal hostnames. If the callback URL in the redirect doesn't match what you registered, this is why — log the generated URL once and compare.
Sessions must survive the round trip. The state and nonce are stored in the session; if your session cookie is SameSite=Strict, or the session driver is array, or the callback lands on a different domain, the state check fails and you get a generic error. SameSite=Lax is what you want, and verify the session cookie domain covers the callback host.
Clock skew and short-lived tokens. Token validation is time-sensitive; containers with drifted clocks fail exp/iat checks intermittently. Run NTP or a time-sync daemon, and don't cache the discovery document for longer than the provider's key rotation interval.
JWKS is cached, so key rotation breaks you eventually. When the IdP rotates signing keys, cached keys cause a run of invalid signature errors. Know where the package caches them (likely Laravel's cache) and know the command to flush it — php artisan cache:clear should be your first move on a sudden auth outage.
Check email_verified before trusting an email. If you use the email for anything security-relevant (invites, password resets, matching to a pre-existing account), require email_verified === true. Linking a pending local account to an incoming OIDC email is an account-takeover vector.
Don't link to pre-existing accounts on email alone. If a user record already exists with that email, decide deliberately: either refuse, or require an explicit admin link. Silent linking is how you get privilege escalation in an app that had local auth before.
Debug with the discovery document and the ID token, not the package. Curl the well-known URL first, then decode a token at jwt.io to see actual claims. Nine times out of ten the bug is a claim your IdP doesn't send, or sends under a different name — a package problem is unlikely and the source is small enough to read.
Assume you may need to fork. One star, zero dependents, and a future-dated release means the bus factor is real. Pin the version in composer.json, read the whole package (it is small), and keep your claims mapping and guard registration in app code rather than subclassing internals — that way replacing it with socialiteproviders/oidc or a ~150-line hand-rolled flow is a day's work rather than a rewrite.
How can I help you explore Laravel packages today?