socialiteproviders/saml2
SAML2 provider for Laravel Socialite that lets Laravel apps authenticate users through SAML 2.0 identity providers. Add enterprise SSO to your existing Socialite login flow with a familiar driver interface and minimal configuration.
This package is a thin Socialite driver over a PHP SAML toolkit. Its value is that enterprise SSO fits the same redirect() / user() shape you already use for GitHub or Google, rather than introducing a second authentication framework. Use it when the IdP speaks SAML2 (Okta, Azure AD/Entra, ADFS, Ping, Shibboleth) and you want Socialite as the application-facing abstraction.
Minimal setup:
composer require socialiteproviders/saml2
Register the driver. On older Laravel skeletons, add the listener to EventServiceProvider; on Laravel 11+, register it in AppServiceProvider or a dedicated provider:
use Illuminate\Support\Facades\Event;
use SocialiteProviders\Manager\SocialiteWasCalled;
use SocialiteProviders\Saml2\Saml2ExtendSocialite;
Event::listen(SocialiteWasCalled::class, Saml2ExtendSocialite::class.'@handle');
Configure the driver in config/services.php. Socialite still expects client_id, client_secret, and redirect; in SAML terms these map to the SP entity ID, SP key material (often unused unless signing/decryption is enabled), and the Assertion Consumer Service URL. The IdP block — SSO URL, entity ID, signing certificate — is driver-specific, so verify the exact keys against the installed README and provider source.
'saml2' => [
'client_id' => env('SAML2_SP_ENTITY_ID'),
'client_secret' => env('SAML2_SP_PRIVATE_KEY', ''),
'redirect' => env('SAML2_ACS_URL', env('APP_URL').'/auth/saml2/callback'),
// IdP metadata / certificate keys per driver version
],
Add the two routes. The redirect is a normal GET; the callback must accept POST, because SAML HTTP-POST binding posts the assertion to the ACS:
Route::get('/auth/saml2/redirect', fn () => Socialite::driver('saml2')->redirect());
Route::post('/auth/saml2/callback', [SamlController::class, 'callback']);
Where to look first: the package README for the exact config shape, the provider class for getAuthUrl() and mapUserToObject(), and your IdP's SAML metadata. Your first use case should be a single IdP and a single test tenant — do not start with multi-tenant routing or encrypted assertions.
Standard SP-initiated login. Let Socialite generate the AuthnRequest, then resolve the user in the callback:
public function callback(Request $request)
{
try {
$samlUser = Socialite::driver('saml2')->user();
} catch (\Throwable $e) {
report($e);
return redirect('/login')->withErrors('SSO failed. Try again.');
}
$user = User::updateOrCreate(
['email' => $samlUser->getEmail()],
['name' => $samlUser->getName()]
);
Auth::login($user, remember: true);
return redirect()->intended('/dashboard');
}
Claim mapping. Socialite's AbstractUser exposes id, name, email, nickname, and the raw payload via $samlUser->user. Most teams map the SAML NameID or immutable ID to a local sso_id, then use email only as a fallback. Avoid keying users by email alone if the IdP allows reassignment. For roles/groups, read the raw assertion attributes and map them explicitly:
$groups = (array) ($samlUser->user['groups'] ?? []);
$user->syncRoles($this->mapGroups($groups));
Preserving the intended URL. SAML's RelayState is the natural place for this, but Socialite's flow and the driver's handling of RelayState vary. A reliable pattern is to store the intended URL in the session before redirecting, then consume it after login. Do not assume the IdP returns arbitrary query parameters on the ACS URL.
Multi-tenant or multiple IdPs. Treat each IdP as a separate configuration and resolve the driver per tenant. Be careful: Socialite caches resolved drivers within the request/container, and config is read when the driver is constructed. In long-running workers, reset or rebuild the driver per request. A common design is one route per tenant (/auth/{tenant}/saml2/...) that swaps services.saml2 config before calling Socialite.
Testing. Unit-test the callback with a mocked Socialite driver (Socialite::shouldReceive('driver')). For integration, run a local IdP (SimpleSAMLphp) or a hosted test IdP, and keep a known assertion fixture. Log the raw SAML response only in local/staging — it contains PII and is security-sensitive.
Integration with existing auth. Keep SAML as an additional guard or login route rather than replacing password auth immediately. A staged rollout — SSO for one team, local login retained for break-glass admins — is much easier to operate.
bootstrap/app.php / VerifyCsrfToken) or handle the token correctly.\n in .env values, missing -----BEGIN CERTIFICATE----- wrappers, or expired IdP signing certs. Validate with openssl x509 -in cert.pem -text -noout.php artisan config:clear. A stale cached entity ID or ACS URL is indistinguishable from an IdP misconfiguration.SameSite cookie issues cause intermittent "invalid response" errors. Use a shared session store in production.NotBefore/NotOnOrAfter windows plus skew cause sporadic failures that are hard to reproduce.client_secret is often unused. Don't assume it is an OAuth client secret. It may represent an SP private key only when request signing or assertion decryption is enabled.SAMLResponse, a redirect with an error parameter, or a library exception. Catch broadly at the controller boundary, log a correlation ID, and show a generic message to the user.mapUserToObject() / config layer. If requirements grow beyond basic SSO, compare against a dedicated Laravel SAML package rather than stretching this driver.http://localhost. Use a tunnel and register that exact ACS URL in the IdP test tenant.with() sparingly — SAML does not have OAuth scopes, and unknown parameters may be ignored.How can I help you explore Laravel packages today?