lightsaml/sp-bundle
Symfony bundle that adds SAML 2.0 Service Provider support via LightSAML. Configure SSO login/logout, consume IdP metadata, and validate assertions with minimal setup—ideal for adding enterprise single sign-on to Symfony applications.
First, the reality check. lightsaml/sp-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, and no config/lightsaml_sp.php. In a Laravel codebase you have two realistic options:
composer require lightsaml/lightsaml (the core library the bundle wraps) and wire the entity descriptors, credential store and request-state store yourself in a service provider. The bundle is essentially ~10 service definitions plus two controllers — you'd be re-implementing that wiring.composer require lightsaml/sp-bundle and register LightSaml\SpBundle\LightSamlSpBundle in config/bundles.php.Be aware of the maintenance signal before committing: last release 2019-11-13, 64 stars, 0 dependents on the tracked registry. That's a Symfony 3/4-era package (lightSAML core 1.x). On Symfony 6/7 or PHP 8.2 you will spend day one fighting deprecations, not doing SSO.
Minimal Symfony bootstrap (if you go that path):
# config/packages/lightsaml_sp.yaml
lightsaml_sp:
host: sp.example.com
entity_id: https://sp.example.com/saml/metadata
credentials:
- certificate: '%kernel.project_dir%/var/certs/sp.crt'
private_key: '%kernel.project_dir%/var/certs/sp.key'
idp:
corp:
entity_id: https://idp.example.com/metadata
single_sign_on_service:
redirect: https://idp.example.com/sso/redirect
certificate: '%kernel.project_dir%/var/certs/idp.crt'
Then import the bundle routing (@LightSamlSpBundle/Resources/config/routing.yml) to get the metadata endpoint, and confirm what actually got registered:
php bin/console debug:router | grep -i lightsaml
php bin/console debug:container lightsaml | head -40
Where to look first: the README's config reference (it is the documentation), then Resources/config/services.xml — that file tells you exactly which services the bundle gives you and is the blueprint for a Laravel port. First use case: SP-initiated SSO — build an AuthnRequest, redirect to the IdP, receive the POSTed SAMLResponse on your ACS route, validate the signature, pull NameID + attributes, log the user in.
The bundle gives you builders, not a firewall. Unlike surfnet/stepup-saml-bundle, there is no security listener doing the work for you. The day-to-day pattern is a pair of controllers around the LightSAML profile builders:
// LoginController::login — SP-initiated SSO
$builder = $this->get('lightsaml.sp.builder'); // verify exact id via debug:container
$context = $builder->buildContext();
return $builder->buildAction()->execute($context); // 302 to the IdP
// AcsController::acs — POST binding, consumes SAMLResponse
$builder = $this->get('lightsaml.sp.response.builder');
$context = $builder->buildContext($request); // pulls SAMLResponse + stored request state
$builder->buildAction()->execute($context); // throws on signature/audience/time errors
$assertion = $context->getAssertion();
$nameId = $assertion->getSubject()->getNameId()->getValue();
$email = $this->attribute($assertion, 'urn:oid:0.9.2342.19200300.100.1.3');
// then authenticate locally (Auth::login() in Laravel, token storage in Symfony)
Request state lives in the session — implement it deliberately. The bundle ships a session-backed store for the AuthnRequest ID + relay state so the ACS can match the response to the request you sent. In Laravel this is the one class you must write: implement the LightSAML request-state storage interface on top of Session::put()/$request->session(), keyed by request ID, with a TTL. If you skip this, you lose replay/matching protection and every response looks "unmatched".
Metadata is the contract — serve it and pin it. Expose the generated metadata XML at a stable URL (e.g. /saml/metadata) and hand it to the IdP admin. Your entity_id, the <AssertionConsumerService Location>, and what the IdP has on file must be byte-comparable. Cache the XML (it changes only when certs rotate) and make sure the route is public — no auth middleware.
Attribute mapping belongs in one class. Don't scatter getAttribute() calls through controllers. Write a small SamlUserMapper that takes the assertion and returns an array/DTO (email, uid, displayName, groups), with configurable attribute names. Then JIT-provision: find-or-create the local user by NameID/email, and treat groups as the role source. This is the piece you will unit test, and it is the piece that breaks when an IdP changes its attribute release.
Multiple IdPs / discovery. Configure each IdP under an alias in the idp: map and carry the alias in relay state or a route parameter. The bundle's discovery controller lets users pick; for most Laravel ports a simple "which IdP?" screen or domain-based routing is less code and less fragile.
Skip SLO unless you must. Single Logout is where SAML integrations rot: partial logout, IdP-initiated logout callbacks, session ordering. Log out locally, redirect to the IdP's SLO URL best-effort, and don't fail the user's logout on an SLO error.
Testing. Unit-test the mapper with fixture assertions and test signature rejection with deliberately tampered XML. For integration, point at a throwaway IdP (samltest.id or a local SimpleSAMLphp) — do not develop against production Okta/Azure AD.
Resources/config/services.xml (tagged services, ContainerAware, parameter placeholders) needs translating. Budget a day for the port; it's small but not zero.composer why-not lightsaml/sp-bundle on your PHP/Symfony version before promising anything. On PHP 8 you'll likely need lightsaml/lightsaml directly with your own wiring rather than the bundle.use_proxy_vars or configure trusted proxies. Laravel: TrustProxies middleware — otherwise the SP thinks its ACS is http:// while the IdP sends https://, and validation fails with a Destination error.VerifyCsrfToken (it's a cross-site POST by design) and from auth. Keep it on web for the session.BEGIN/END CERTIFICATE lines) exactly as the config expects; extra whitespace or a missing newline yields signature verification failures that read like "invalid signature" rather than "bad cert".NotBefore / NotOnOrAfter; a minute of drift on the IdP gives "assertion is not yet valid". Keep SP and IdP on NTP.base64_decode($request->input('SAMLResponse')) before running validation, and correlate with the request ID you stored. LightSAML throws specific exceptions (LightSamlValidationException, XML parse errors) — log the raw XML alongside them or you'll be guessing.How can I help you explore Laravel packages today?