overtrue/laravel-saml
A Laravel package for SAML 2.0 single sign-on. It provides configurable identity provider settings, login/logout routes, and a straightforward integration for adding SSO authentication to Laravel applications.
Check your platform first. 2.x requires PHP 8.3+ and Laravel 13.x. If you're on Laravel 9–12, stay on the 1.x line — pin "overtrue/laravel-saml": "^1.0" and don't let a composer update drag you across the major. The 2.0.0 release is a compatibility/maintenance major, not a feature major: SAML APIs, config keys, security defaults, validation policy, and logout response behaviour are all unchanged.
Install and publish:
composer require overtrue/laravel-saml:^2.0
php artisan vendor:publish --tag=saml-config # check the exact tag in the README
First place to look: the published config/saml.php. It holds the SP block (entity ID, ACS/SLS URLs, signing key + cert) and the IdP block (SSO/SLO endpoints, x509 cert, binding preferences). Everything you'll debug later traces back to these values.
First use case — SP-initiated login against a real IdP (Okta, Entra ID, Auth0, Keycloak, ADFS):
/saml/metadata by default) and upload it to the IdP, or copy the entity ID + ACS URL in manually.NameID / attributes onto your User model and Auth::login() them.Get that single flow working end-to-end before touching logout, SLO, or multi-IdP — it exercises config, certificates, clock skew, sessions, and routing in one shot.
Prefer the package's route set; override only the handler. The default metadata / login / ACS / SLS routes plus the package middleware are what the release notes explicitly tell you to preserve. The clean extension point is your own controller or listener for the "consume assertion → provision user" step, not re-implementing the protocol plumbing:
// ACS: assertion is already validated by the package at this point
$nameId = $samlUser->getNameId();
$email = $samlUser->getAttribute('email')[0] ?? $nameId;
$user = User::firstOrCreate(['email' => $email]);
Auth::login($user, remember: true);
return redirect()->intended('/dashboard');
Keep JIT provisioning idempotent and keyed on a stable attribute — NameID format varies wildly between IdPs (email vs. persistent opaque ID), so decide early and document it.
Multi-tenant / per-request IdP resolution. The 2.0.0 fix ("explicit IdP settings incorrectly invoking the configuration resolver") is the one that matters day-to-day: if you resolve an IdP per tenant/domain and pass the settings explicitly, those explicit values now win instead of being re-run through the resolver. Pattern:
$idp = Tenant::findByDomain($request->getHost())->samlConfig(); // array
Saml::withIdp($idp)->redirect(); // explicit settings are honoured as-is
If you previously worked around this by mutating config() at runtime, revisit that workaround — it may now be redundant, and config() mutation is fragile under config:cache anyway.
Signed responses vs. signed assertions. Most IdPs let you choose. Decide which one you validate and make it a config invariant, not a per-environment accident. 2.0.0 adds regression coverage for signed-response acceptance and unsigned/tampered-response rejection — mirror that in your own suite rather than trusting a green manual test.
Logout: handle both directions. SP-initiated logout goes through the IdP's SLO endpoint; IdP-initiated SLS arrives as an unsolicited POST to your SLS route. 2.0.0 explicitly preserves the logout response behaviour, so existing 1.x SLO integrations should carry over — verify anyway, since SLO is the most commonly broken part of any SAML setup.
Testing pattern. Use the package's new provider/metadata/login/logout/authentication tests as a template. For each environment-critical assertion, add three fixtures: valid signed, unsigned, and tampered (flip a byte in the assertion). Also cover request-ID mismatch — the release notes confirm this is now a tested rejection path, and it's the check that catches replay and stale-session bugs.
The upgrade is a Laravel-version gate, not a code rewrite. The only breaking change is the platform requirement. The real risk is silent breakage if you upgrade Laravel and the SAML package in the same window — do the Laravel 13 bump first, verify the app boots, then move the SAML package.
Follow the upgrade guide checklist explicitly. It calls out the four things that actually cause incidents: IdP certificates, strict validation mode, signing requirements, and your application-specific ACS/SLS routes and middleware. Diff your config/saml.php against the new stub and re-apply your customisations rather than overwriting.
Preserve strict mode and signing config. It's tempting to relax strict or wantAssertionsSigned when a staging IdP misbehaves. Don't ship that. Fix the IdP metadata or the certificate instead.
Certificate handling is the #1 support ticket. IdP certs rotate (often annually, sometimes with no warning), and the failure surfaces as an opaque assertion-validation error. Store the cert in config/env with a clear expiry note, and know how to swap it under pressure. Strip or preserve PEM headers deliberately — some IdPs publish certs without -----BEGIN CERTIFICATE----- and php-saml-based stacks will reject the mismatch. Check your clock/NTP too; even small skew breaks NotBefore/NotOnOrAfter.
CSRF and cookies break the POST binding. The ACS endpoint receives a cross-site POST from the IdP. Ensure the ACS route is excluded from CSRF and that session cookies are not blocked by SameSite=Strict. Behind a load balancer or TLS terminator, configure trusted proxies so generated ACS URLs stay https — a mismatched scheme produces an InResponseTo/destination validation failure that looks like a protocol bug.
Error handling got better — write your catch blocks accordingly. 2.0.0 preserves the original PHP Error/TypeError instead of raising a secondary TypeError inside AssertException. Two implications: (a) your logs will now show the real root cause, so re-read old incident notes with that in mind; (b) don't blanket-catch TypeError as a proxy for SAML failures any more — catch the package's SAML/assertion exception types and let genuine programmer errors surface.
config:cache is a footgun. Never mutate SAML config at runtime via config() in a way that only works uncached; always re-run php artisan config:cache after changing IdP values in deployed environments, and prefer explicit-settings calls (which 2.0.0 now handles correctly) over global mutation.
Debugging toolkit: SAML-tracer browser extension for inspecting the actual SAMLResponse, the IdP's own event/audit log, and the package's debug output (local only — it logs assertions, which are credentials). Keep a known-good signed SAMLResponse fixture around for offline comparison.
No live IdP test was performed for this release. Validation was 26 tests / 100 assertions across PHP 8.3–8.5, but the maintainers explicitly did not test against a real identity provider. After upgrading, manually verify login, assertion validation, and logout against your IdP in a staging environment before this reaches production — that gap is exactly where the certificate and clock-skew issues live.
Extension points worth knowing: custom route definitions/middleware overrides in config, a custom ACS controller, and event/listener hooks around authentication if your config exposes them. Reach for these before forking the package — the protocol layer is the part you least want to own.
How can I help you explore Laravel packages today?