nl.idaas/laravel-openid-connect
Add OpenID Connect authentication to Laravel with a configurable OIDC client for standards-compliant identity providers. It handles provider discovery, authorization redirects, callback processing, and user login, making secure single sign-on easier to integrate.
1. Install and swap out Passport
composer require nl.idaas/laravel-openid-connect
Because this is advertised as a drop-in replacement for Passport, the first decision is whether your app already uses laravel/passport. Check vendor/nl.idaas/laravel-openid-connect/composer.json:
replaces laravel/passport, remove laravel/passport from your require block and re-run composer update. Leaving both installed will double-register the /oauth/* routes and the passport guard, causing redirect loops and 404/405 confusion.2. Wire it up
Laravel\Passport\HasApiTokens is typically reused) to your User model.auth:api with driver passport in config/auth.php.php artisan vendor:publish --provider="NL\IDaaS\LaravelOpenIDConnect\LaravelOpenIDConnectServiceProvider"
php artisan migrate
php artisan passport:keys and passport:client if the package doesn't ship its own equivalents — check the README for the exact command names, as these have shifted across versions).3. Register OIDC scopes
In AuthServiceProvider::boot() you must declare openid (and usually profile, email, offline_access) via Passport::tokensCan([...]). Without openid, every authorize request fails with invalid_scope — this is the #1 first-run stumbling block.
4. First use case: SSO for a first-party SPA
Create a public client (no secret, PKCE required), point it at your SPA's redirect URI, and verify the whole flow with oidcdebugger.com or oauth.tools:
GET /.well-known/openid-configuration — confirm all endpoints and the issuer look right.GET /oauth/authorize?response_type=code&client_id=...&scope=openid profile email&redirect_uri=...&state=...&code_challenge=.../oauth/token with the PKCE code_verifier./oauth/userinfo with the access token to confirm claims.Where to look first: the package's README, the published config/ file, and the route list (php artisan route:list --path=oauth).
Retrofitting an existing Passport deployment. Existing oauth_clients rows generally survive because the schema is Passport-compatible, but the package may add OIDC-specific columns. Run php artisan migrate --pretend first, and snapshot the oauth_clients table before migrating in production. Existing clients keep working; you only need to ensure openid is an allowed scope for clients that will now request it.
Auth-code + PKCE for SPAs and mobile. Public client, exact-match redirect URI, response_type=code, S256 challenge, request offline_access if you need refresh tokens. Refresh tokens are only issued when offline_access is in the granted scopes — don't assume they come for free with openid.
One client per relying party. For partner integrations, register a dedicated client with a minimal scope set and a per-client redirect URI. This gives you per-RP revocation and clean audit trails. Pair it with the default consent screen (publish and override the authorization view if you need branding) so users see exactly which claims are being shared.
Custom claims. For anything beyond sub/name/email, override the userinfo response or hook the id-token claim assembly (the package typically exposes a builder or event). Keep claims stable: changing the sub value later will break account linking in every downstream RP. Decide early whether sub is a UUID, an opaque hash, or the raw user ID, and document it.
Testing in CI. For feature tests, use Passport::actingAs($user, ['openid']). For an end-to-end test of the flow, drive /oauth/authorize with a session cookie via $this->actingAs($user)->get(...), parse the Location header for code, then POST to /oauth/token with the PKCE verifier. Don't unit-test the discovery endpoint in isolation — fetch it once and assert issuer matches config('app.url').
Federation downstream. Point Kong, Traefik, NextAuth, or another OIDC consumer at your /.well-known/openid-configuration. Most clients auto-configure from discovery; the only manual step is registering the client ID/secret with them.
issuer in your discovery document must byte-for-byte match the iss claim in tokens and the value relying parties were configured with. Behind a load balancer or proxy, $request->getSchemeAndHttpHost() will fall back to http:// and cause silent invalid issuer errors. Set APP_URL correctly and force HTTPS (URL::forceScheme('https')) in a service provider.openid scope must be registered. invalid_scope on authorize almost always means you forgot Passport::tokensCan(['openid' => ...]) or forgot to add the scope to the specific client.nonce must be echoed in the id_token when the client sends one. Some lightweight OIDC implementations omit it; verify with a debugger if a SPA library rejects the id_token.jwt.decode() the access token will fail — they must use /oauth/userinfo or introspection. If a downstream tool requires a JWT access token, verify the package (or a Passport extension) supports it before promising the integration.kid and shared storage. If you run multiple app servers, passport:keys must write to a shared volume, and JWKS must be served consistently. Rotating keys without advertising a new kid in JWKS will invalidate every token in flight.http://localhost in production mode. Configure separate clients for local dev and prod — don't reuse.client_secret_basic; some consumers only implement client_secret_post. Public clients must use none with PKCE. If the token endpoint returns invalid_client, it's almost always this mismatch.Cache-Control on discovery and JWKS; relying parties cache by kid, and a stale JWKS is a common cause of "signature verification failed" after rotation.How can I help you explore Laravel packages today?