# OpenID Connect
A generic OpenID Connect driver for Laravel Socialite. Point it at any issuer that serves a discovery document: Keycloak, Entra ID, Auth0, Okta, Google, Authentik, or anything else that speaks OIDC. You can configure several issuers at once, and each becomes its own Socialite driver.
Endpoints come from the issuer's discovery document. Signing keys come from its JWKS and refresh automatically when the issuer rotates them. Every id_token is validated properly (signature, iss, aud, azp, exp, nonce, at_hash) and PKCE is on by default.
composer require socialiteproviders/openidconnect
# Configure
Publish the config file:
php artisan vendor:publish --tag=oidc-config
Add one entry per issuer to config/oidc.php:
return [
'connections' => [
'keycloak' => [
'provider' => 'keycloak',
'server_url' => env('KEYCLOAK_SERVER_URL'), // https://id.example.com
'realm' => env('KEYCLOAK_REALM'),
'client_id' => env('KEYCLOAK_CLIENT_ID'),
'client_secret' => env('KEYCLOAK_CLIENT_SECRET'),
'redirect' => env('KEYCLOAK_REDIRECT_URI'),
],
'entra' => [
'provider' => 'entra',
'tenant' => env('ENTRA_TENANT', 'common'),
'client_id' => env('ENTRA_CLIENT_ID'),
'client_secret' => env('ENTRA_CLIENT_SECRET'),
'redirect' => env('ENTRA_REDIRECT_URI'),
],
// Any OIDC-compliant issuer works with just its base URL:
'authentik' => [
'base_url' => env('AUTHENTIK_BASE_URL'),
'client_id' => env('AUTHENTIK_CLIENT_ID'),
'client_secret' => env('AUTHENTIK_CLIENT_SECRET'),
'redirect' => env('AUTHENTIK_REDIRECT_URI'),
],
],
];
Each connection becomes a Socialite driver named oidc_{connection}. Connections share nothing: each has its own credentials, endpoints and caches.
# Use
Vanilla Socialite from here:
// Send the user to the IdP:
return Socialite::driver('oidc_keycloak')->redirect();
// In your callback controller:
$user = Socialite::driver('oidc_keycloak')->user();
$user->getId(); // sub, the one claim every IdP must return
$user->getEmail(); // null when the IdP didn't grant an email
$user->getName();
$user->getRaw(); // every claim from the id_token (merged with userinfo)
Everything available on the returned user
$user->getId(); // sub
$user->getRaw(); // all claims
$user->accessTokenResponseBody['id_token']; // the raw id_token, stash it for logout
$user->approvedScopes; // scopes the IdP actually granted
$user->token; // access token
$user->refreshToken; // refresh token, if granted
Mapped fields: id (sub), email, name, nickname, given_name, family_name, idp, role, groups.
email isn't guaranteed. It depends on the email scope being granted, so a user without one gets a null email rather than a failed login. Set require_email if your application can't proceed without it. When the id_token has no email, the userinfo endpoint is consulted automatically and its claims merged in.
Access and refresh tokens are bearer credentials. If you persist them, encrypt them at rest (Laravel's encrypted cast) and keep them out of JavaScript-readable storage.
For what comes after login (RP-initiated logout with state validation, RFC 7009 token revocation, back-channel logout, refreshing tokens, storing tokens safely) see docs/logout.md.
# Built-in providers
The provider key picks the class that drives a connection. The built-in classes know the URL shape for their IdP, so you set tenant or realm or domain instead of building the base URL yourself. Anything you set explicitly wins over what the class derives:
provider | Reads | Derives |
|---|---|---|
entra | tenant (default common) | base_url, multi-tenant issuer handling (details), email from preferred_username (why) |
keycloak | server_url, realm | base_url = {server_url}/realms/{realm} |
auth0 | domain | base_url, client_secret_post token auth |
okta | domain, auth_server | base_url, /oauth2/{auth_server} when named |
google | none | base_url = https://accounts.google.com |
For any other issuer, omit provider and set base_url directly. To encode your own IdP's shape, write a provider class and put its class name in provider. docs/extending.md covers the configDefaults() hook, the commonly overridden methods, and issuer validation (including how Entra multi-tenant is handled).
# Configuration reference
All connection keys
Required: client_id, client_secret, redirect, and base_url (unless a built-in provider derives it).
| Key | Default | Meaning |
|---|---|---|
base_url | none | Issuer URL; discovery is {base_url}/.well-known/openid-configuration. https required (loopback hosts exempt, so local dev works). |
provider | Provider::class | Built-in shorthand or a Provider subclass name. |
scopes | openid email profile | Replaces the defaults. Array, or string separated by whitespace/commas. openid is always sent. |
email_claims | ['email'] | Claims consulted for the user's email, first non-empty wins. The entra provider defaults this to ['preferred_username'] (why). |
verify_jwt | true | Verify id_token signatures. Only disable for an OP that can't serve a JWKS; back-channel logout tokens are always verified regardless. |
jwt_public_key | none | PEM public key used instead of fetching the JWKS. |
jwt_algorithm | advertised algs, else RS256 | Pin the accepted signing algorithm(s), e.g. RS256 or RS256,ES256. |
issuer | discovery issuer | Override the expected iss claim. |
issuer_validator | strict equality | IssuerValidator class, for issuers an exact comparison can't handle. |
token_auth_method | advertised, preferring basic | client_secret_basic or client_secret_post. |
use_nonce | true | Send and validate a nonce. Ignored (always off) in stateless mode. |
require_email | false | Fail the login when no email can be obtained. |
post_logout_redirect_uri | none | Default for the logout() helper. |
logout_token_replay_ttl | token exp + skew | Seconds a back-channel logout jti is remembered. 0 disables built-in replay protection. |
cache_ttl | 3600 | TTL for the cached discovery document and JWKS. |
clock_skew | 0 | Leeway in seconds applied to exp/nbf/iat. |
http_timeout / http_connect_timeout | 10 / 5 | Guzzle timeouts for IdP calls. |
proxy | none | Proxy for IdP calls, in Guzzle's format (opens new window) (a URL string, or an array per scheme). |
The driver_prefix config key (default oidc_) controls the driver names.
Single issuer without a config file
A plain services.php entry registers an openidconnect driver with no config/oidc.php at all. The same keys apply, including provider:
'openidconnect' => [
'base_url' => env('OIDC_BASE_URL'),
'client_id' => env('OIDC_CLIENT_ID'),
'client_secret' => env('OIDC_CLIENT_SECRET'),
'redirect' => env('OIDC_REDIRECT_URI'),
],
# Security
Every login validates the state (CSRF), the id_token signature against the discovered JWKS (refetched when keys rotate), the signing algorithm against an allow list, iss, aud/azp, exp/nbf/iat, the nonce (cleared after use, so replays fail), at_hash against the access token, and sub. PKCE is on by default. docs/security.md has the full detail.
Stateless mode and PKCE
The nonce and the PKCE verifier both live in the session between the redirect and the callback:
->stateless()skips the nonce automatically. That's safe for the code flow, where the code is bound to the client by PKCE and exchanged over the back channel. PKCE still works as long as a session store is bound to the request.- With genuinely no session, add
->withoutPKCE():
Socialite::driver('oidc_keycloak')->stateless()->withoutPKCE()->redirect();
withoutNonce() disables the nonce alone.
# Testing
composer test
The suite covers discovery and caching, JWKS key rotation, algorithm and claim validation, nonce replay, userinfo merging, token endpoint auth methods, the logout flows, the built-in provider classes, and multi-connection isolation.
# Credits
The provider implementation originates from SocialiteProviders/Providers#1447 (opens new window) by adrum (opens new window).
# License
MIT
← Contribute Adobe →