jeffersongoncalves/laravel-oidc
Laravel OIDC integrates OpenID Connect authentication into Laravel applications. It helps configure providers, redirect users to sign in, validate ID tokens, and retrieve user profile data with a simple, framework-friendly setup.
Install and publish config first.
composer require jeffersongoncalves/laravel-oidc
php artisan vendor:publish --tag="oidc-config"
Open config/oidc.php immediately — this is the single source of truth for issuer URL, client ID/secret, redirect URI, scopes, and (as of v1.3.0) identities_table. Read it top to bottom before touching any model; most confusion in this package comes from not knowing which key drives which behaviour.
Decide up front whether you need identity linking (v1.3.0+).
By default the package authenticates users against the OIDC provider. The new OidcIdentity model + HasOidcIdentities trait exist to persist the link between a local authenticatable and an external (issuer, subject) pair. If you only need "log in via OIDC and create/find a user", you can skip publishing the migration entirely — it's optional by design.
Publish and run the identity migration if you want linking.
php artisan vendor:publish --tag="oidc-migrations"
php artisan migrate
Before migrating, set oidc.identities_table in config if oidc_identities collides with your naming convention. The table name is read from config, so changing it after migrating means a manual rename + config change.
First use case to get working: a happy-path login.
Wire your routes/web.php (/auth/redirect → package redirect, /auth/callback → package callback), register the redirect URI at your IdP exactly as configured (scheme, host, port, trailing slash all matter), and log in. Only once that works should you add identity linking — debugging both at once is painful because failures look similar (silent redirect loops vs. uniqueness violations).
Where to look when stuck: config/oidc.php → the package's callback/controller classes → storage/logs/laravel.log. Discovery/JWKS failures are almost always logged with the upstream HTTP status, which tells you instantly whether it's your config or the IdP.
Pattern: trait on the user model (v1.3.0).
use JeffersonGoncalves\Oidc\Traits\HasOidcIdentities;
class User extends Authenticatable
{
use HasOidcIdentities;
}
This is the intended integration point. Add it to the single model that represents your login principal. Adding it to multiple models (e.g. User and Admin) is supported by the schema (the identity table is polymorphic in spirit) but means your callback handler must decide which guard/model to resolve — usually cleaner to run separate OIDC client configs per guard.
Pattern: treat (issuer, subject) as the primary key of trust.
The unique constraint is on issuer + subject, not on email. This is the correct OIDC semantic and it matters in practice: users can change email at the IdP, and two different issuers can legitimately assert the same email. Always look up an existing identity by issuer+subject before falling back to email matching, and if you do fall back to email, only auto-link when the issuer is one you control/trust — otherwise you have an account-takeover vector.
Pattern: separate "login" from "link an additional provider". Because identities are a table now, you can support a user with a password and an OIDC identity, or two OIDC identities from different issuers. Structure your callback handling as:
(issuer, subject) → identity found → log in identity->authenticatablePattern: keep config environment-driven.
Put OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI in .env and reference them from config/oidc.php. Never hardcode the issuer per environment — discovery URLs and JWKS endpoints derive from it, and staging/production tokens will not validate against each other.
Pattern: middleware for route protection.
Keep package routes for redirect/callback, and wrap your own app routes in auth middleware as usual. Don't put the callback route behind auth — the whole point is that the user isn't authenticated yet during callback.
Pattern: local development without an IdP. Use a real IdP's free tier / a local Keycloak or Dex container rather than mocking the package internals. The package's job is protocol correctness (state, nonce, PKCE, JWKS rotation); mocking it tests nothing useful.
The Guzzle constraint fix matters for Laravel 11/12. v1.3.0 relaxes guzzlehttp/guzzle back to ^7.8. If you were pinned to an older package version because composer update failed on a Laravel 11 or 12 app, upgrade now — and run composer why-not guzzlehttp/guzzle if you still have conflicts, since your app or another package may also be pinning it.
identities_table is config, not a constant. If you change it after migrating, nothing renames the table for you. Either lock the name before your first production migration or write an explicit rename migration.
Optional means optional — but not free. Publishing the migration without adding the trait to your model leaves you with an unused table. Conversely, adding the trait without running the migration gives you "table not found" errors only at callback time, which is a confusing place to discover it. Do both or neither.
Unique constraint violations are a feature, not a bug. Concurrent first-logins from the same user can race and both try to insert the identity. Wrap identity creation in a firstOrCreate on (issuer, subject) and catch the unique violation to re-fetch, rather than letting a 500 surface. This is the most likely production-only bug you'll hit with the new table.
Redirect URI mismatch is the #1 setup failure. It must match the IdP registration byte-for-byte, including trailing slash and http vs https. Behind a proxy or load balancer you'll also need TrustProxies configured so Laravel generates https:// URLs — otherwise the redirect URI sent differs from the one registered.
Check token validation, not just "did I get a session". Signature (JWKS), iss, aud, exp, and nonce/state all need to pass. If logins succeed suspiciously easily in one environment, that's worth a second look — validate against a token with a deliberately wrong audience to confirm rejection is real.
Clock skew. If JWKS or token validation fails intermittently with "not yet valid" / "expired", check server time (timedatectl, NTP). This is a classic false alarm that sends people digging through the package source.
Debugging extension point: the HTTP client. Most issues are outbound calls to the discovery document or JWKS endpoint. Log the discovery response body (/.well-known/openid-configuration) once — it tells you the exact authorization_endpoint, token_endpoint, and jwks_uri the package will use, so you can verify them by hand with curl before blaming your app code.
IdP-side logout is out of scope. Session invalidation at the provider needs end_session_endpoint from discovery and an id_token_hint. Expect to implement that yourself around the package rather than expecting a config flag.
How can I help you explore Laravel packages today?