mlightsaml/symfony-bridge
Symfony bridge for the LightSAML PHP library, adding SAML 2.0 service provider support to Symfony applications. Integrates SAML authentication with Symfony's security system and provides configuration for identity providers, certificates, and routes.
Reality check before you type composer require. mlightsaml/symfony-bridge is a Symfony bundle, not a Laravel package. There is no service provider, no Facade, no config/saml.php auto-publish. If you composer require it into a Laravel app, Composer may resolve it (Laravel already ships Symfony HttpFoundation/Console/Routing), but the bundle's real dependencies — symfony/security-bundle, symfony/framework-bundle — are not present, and pulling them in drags a second kernel/container into your app. Treat the repo as reference code, not a dependency. Read it, copy the wiring ideas, throw the bundle away.
Where to look first (in the repo, not in your app):
src/DependencyInjection/*Extension.php and src/Resources/config/services.* — this is the documentation. The bundle's only job is to register LightSAML core services (credential stores, entity descriptor stores, binding factory, profile builders). That service graph is what you'll port into a Laravel service provider.vendor/lightsaml/lightsaml/ after a spike install — the real API. The bridge is thin; all protocol code lives upstream.lightsaml/lightsaml's LightSaml\Builder\* and LightSaml\Profile\* namespaces — that's what you'll actually call from a Laravel guard/middleware.Your first use case. Don't try to be an IdP. The 90% case is SP-initiated SSO against one IdP (Okta, Azure AD, Google Workspace). Sketch the flow before writing code:
GET /saml/login → build a AuthnRequest, store RelayState + request ID in cache, redirect to IdP SSO URL.POST /saml/acs → parse SAMLResponse, validate signature against IdP cert, check Audience, Recipient, NotBefore/NotOnOrAfter, replay-check assertion ID, then Auth::login($user).GET /saml/metadata → emit SP metadata XML so the IdP admin can paste it in.Minimal config surface you'll want to mirror from the bundle's config tree:
// config/saml.php — your own file, not the bundle's
return [
'sp' => [
'entity_id' => env('SAML_SP_ENTITY_ID', 'https://app.example.com/saml/metadata'),
'acs_url' => env('SAML_SP_ACS_URL', 'https://app.example.com/saml/acs'),
'sls_url' => env('SAML_SP_SLS_URL', 'https://app.example.com/saml/sls'),
'cert' => storage_path('saml/sp.crt.pem'),
'key' => storage_path('saml/sp.key.pem'),
],
'idp' => [
'entity_id' => env('SAML_IDP_ENTITY_ID'),
'sso_url' => env('SAML_IDP_SSO_URL'),
'sls_url' => env('SAML_IDP_SLS_URL'),
'cert' => storage_path('saml/idp.crt.pem'),
],
];
Bind it into a SamlServiceProvider with a LightSaml\Builder\Profile\WebBrowserSso\Sp\SsoSpReceiveResponseBuilder (or the equivalent builder — check the LightSAML version) behind your own SamlAuthenticator contract so you can swap libraries later.
Pattern 1 — Own the glue, not the bundle. Wrap LightSAML in a single bound interface your app calls:
interface SamlAuthenticator
{
public function initiate(string $relayState = null): string; // returns IdP URL
public function consumeResponse(string $samlResponse): SamlUser; // throws on invalid
public function metadata(): string; // SP metadata XML
}
Bind the LightSAML implementation in AppServiceProvider, and a fake in tests. This is the single biggest win: it lets you swap to onelogin/php-saml or a managed provider with one class. The Symfony bridge proves this pattern works — it's exactly what it does for Symfony's DI container. You're just doing the Laravel equivalent.
Pattern 2 — Controller + Guard separation. Keep SAML endpoint handling in dedicated controllers (SamlController with login, acs, sls, metadata), and let them call the authenticator. Do not try to shoehorn SAML into a custom Guard — the request/response lifecycle (ACS is a cross-site POST with a signed XML body, not a login form) fights Laravel's auth guards. A controller that calls Auth::login() on success is cleaner and easier to test.
Pattern 3 — RelayState as a cache-backed redirect target. RelayState is the IdP's only way to round-trip where to send the user after login. Store the intended url.intended (or the raw RelayState) in cache keyed by AuthnRequest ID at login, retrieve it at ACS:
Cache::put("saml:req:{$requestId}", [
'relay' => $relayState,
'expires' => now()->addMinutes(15),
], now()->addMinutes(15));
Pattern 4 — Attribute mapping as config. IdPs emit wildly different attribute names (urn:oid:0.9.2342.19200300.100.1.3 vs http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress vs mail). Keep a mapping table and default to a unique identifier (email or NameID), never a display name:
'attributes' => [
'email' => ['urn:oid:0.9.2342.19200300.100.1.3', 'email', 'mail'],
'name' => ['urn:oid:2.16.840.1.113730.3.1.241', 'displayName'],
'groups'=> ['http://schemas.xmlsoap.org/claims/Group'],
],
Pattern 5 — Provisioning on ACS, not before. Decide at the ACS endpoint whether an unknown NameID creates a user (JIT provisioning), is rejected, or maps to a pre-existing account. For B2B this is usually "match on email domain, JIT-create with a default role" — write it as an explicit policy class, not scattered ifs.
Pattern 6 — Per-tenant IdP config from DB. The multi-tenant version (each customer brings their own IdP) is where SAML integrations get hard. Model it: tenants table with idp_entity_id, idp_sso_url, idp_cert, attribute_map, enabled. The login route becomes /{tenant}/saml/login. The bundle's static YAML config model does not help you here — this is app schema work regardless of library.
Zero adoption = zero safety net. 0 stars, 0 dependents, single maintainer fork. You are the community. No one else is finding PHP-8.x breakage in the XML-DSig path before you do. If you adopt this at all, name a maintainer, add it to your security review queue, and subscribe to lightsaml/lightsaml advisories directly — not this bundle's, since it probably won't publish them.
Do not install the bundle in Laravel. The only reason to composer require mlightsaml/symfony-bridge is a throwaway spike app to read the service graph under bin/console debug:container. In production, depend on lightsaml/lightsaml directly behind your own interface. Bundling Symfony's Security into Laravel is an adaptor project, not an integration.
Signature validation is the whole game. The bridge's code path is thin; the security-critical code is in LightSAML core. In the spike, verify:
Signature element is validated against the IdP's configured cert, not the one embedded in the response. (This is the classic SAML vuln.)Audience equals your SP entity ID.Recipient equals your ACS URL.NotBefore / NotOnOrAfter are checked (skew tolerance ≤ 2 minutes) against your server's clock — NTP-drift your nodes.libxml_disable_entity_loader semantics, DTD rejection). PHP 8 defaults help, but confirm.Replay protection needs shared state. The single most common production bug in multi-node Laravel apps: assertion-ID replay cache is per-node (file or array cache), so a replay against a different node passes. Put the assertion-ID store in Redis with a TTL covering NotOnOrAfter plus skew. Test it with two app nodes before declaring victory.
RelayState is not trusted input. It's attacker-controllable (bounced off the IdP). Validate it's a same-origin path or a whitelisted URL before redirect(), or you're shipping an open redirect.
Certificate rotation is on you. No upstream alerting, no openssl monitoring from the bundle. Add a scheduled Artisan command that checks the IdP cert and SP cert notAfter and alerts at 30/14/7 days. Same for IdP metadata if you consume a metadata URL.
Metadata XML, not hand-written. Generate SP metadata from your config with LightSaml\Model\Metadata\* rather than templating XML. Any drift between your metadata and your actual ACS/cert breaks IdP setup, and the symptom is a generic "invalid response" at the IdP side with nothing in your logs.
Clock skew bites silently. IdP nodes and your app nodes will disagree by seconds. Set NotBefore tolerance explicitly (LightSAML exposes it via the validation context — find it, don't rely on zero) and alert on NotBefore/NotOnOrAfter validation failures separately from general signature failures so you can distinguish "clock" from "tampering" in the on-call runbook.
Log the SAML response ID and issuer on every ACS, never the payload. You'll need to correlate an IdP-side failure to a request ID, but a raw SAML response can contain PII and session tokens. Log response_id, issuer, subject, in_response_to, audience, and outcome — that's the debuggable minimum.
Testing is a build step, not an afterthought. You need a local IdP (SimpleSAMLphp in Docker is fine) and fixtures for: valid login, expired assertion, wrong audience, bad signature, replay, unknown user, IdP-initiated (if in scope). These fixtures are reusable if you later swap to onelogin/php-saml — which is exactly why the interface abstraction in Pattern 1 matters.
Consider rejecting this package. The honest recommendation for a Laravel team: use onelogin/php-saml (maintained, SP-focused, no Symfony baggage) behind the same SamlAuthenticator interface, or buy managed SSO (WorkOS, Auth0). The only reason to touch LightSAML at all is a hard requirement it uniquely satisfies — IdP-mode, artifact binding, encrypted assertions, or an existing LightSAML deployment you're inheriting. If you're not in one of those buckets, this bundle is a liability dressed as a shortcut.
How can I help you explore Laravel packages today?