javer/symfony-bridge
A lightweight bridge for integrating Symfony components into Laravel applications, enabling seamless interoperability between service containers, event dispatchers, HTTP foundation, and other Symfony packages in mixed Symfony/Laravel projects.
Check your PHP and Symfony versions first. As of 1.6.0 the bridge requires PHP >= 8.1 and supports Symfony 7 (alongside the earlier majors). On a Symfony 7 app this is moot — Symfony 7 itself already requires PHP >= 8.2 — but on Symfony 5.4/6.x with PHP 8.0 or 7.4 you must stay pinned to 1.5.x. Set the constraint explicitly rather than relying on ^1.0 resolution drift:
composer require javer/symfony-bridge:^1.6 # PHP 8.1+, Symfony 6/7
composer require javer/symfony-bridge:^1.5 # older PHP/Symfony
lightsaml/lightsaml is the actual SAML engine; the bridge is a thin Symfony DI wrapper around it. Register the bundle class (check src/ for the exact namespace — traditionally LightSaml\SymfonyBridgeBundle\LightSamlSymfonyBridgeBundle) in config/bundles.php. On Symfony 6/7 that is the only registration step; there is no Flex recipe.
Upgrading from 1.5.x: the 1.6.0 notes list no breaking changes — only Symfony 7 support, the PHP floor, and CI updates. Expect composer update && bin/console cache:clear to be enough, but re-run bin/console debug:container lightsaml --show-private afterwards to confirm the service IDs below survived the DI refactor, and re-run your SAML fixture tests before deploying.
Minimal config. The bundle's configuration tree is the first file to read: DependencyInjection/Configuration.php. Expect something close to:
# config/packages/light_saml_symfony_bridge.yaml
light_saml_symfony_bridge:
own:
entity_id: "https://app.example.com/saml/metadata"
credentials:
- certificate: "%kernel.project_dir%/config/saml/sp.crt"
private_key: "%kernel.project_dir%/config/saml/sp.key"
party:
idp:
file: "%kernel.project_dir%/config/saml/idp.xml" # IdP metadata XML
That config compiles into a small set of services — typically lightsaml.system.own_entity_descriptor, lightsaml.system.idp_entity_descriptor, the credential/trust stores, a time provider, and lightsaml.container.build, which is the factory container you'll actually use in controllers. Read Resources/config/services.xml to confirm the IDs before you autowire anything; don't guess.
First use case: SP-initiated login. Two endpoints, both hand-written controllers:
// LoginController::start
$build = $this->get('lightsaml.container.build');
$own = $build->getSystemContainer()->getOwnEntityDescriptor();
$idp = $build->getSystemContainer()->getIdpEntityDescriptor();
$request = new AuthnRequest();
$request->setID(uniqid('', true));
$request->setIssuer(new Issuer($own->getEntityID()));
$request->setDestination($idp->getFirstSsoServiceLocation(Constants::BINDING_HTTP_REDIRECT));
$request->setAssertionConsumerServiceURL($this->generateUrl('saml_acs', [], UrlGeneratorInterface::ABSOLUTE_URL));
$request->setProtocolBinding(Constants::BINDING_HTTP_POST);
$session->set('saml_request_id', $request->getId());
$build->getBindingFactory()->create(Constants::BINDING_HTTP_REDIRECT)->send($request)->send();
Prefer injecting lightsaml.container.build into the constructor over $this->get() — it is clearer, works the same on Symfony 6/7, and makes the dependency visible to static analysis.
Then an ACS controller that receives the POST, decodes the SAMLResponse, validates it, and either logs the user in or throws. Do not skip the request-ID check — see Gotchas.
If you are evaluating this package rather than committing to it: with 0 stars and 0 dependents, read the whole src/ tree (it is small) before wiring it into anything production-facing. 1.6.0 shows someone is keeping it buildable against current Symfony, but the upstream lightsaml/symfony-bridge this forks is still largely dormant scaffolding.
Treat the bundle as a container, not a framework. It gives you a correctly-constructed SystemContainer (own descriptor, IdP descriptor, credential store, trust store, time provider, binding factory) and nothing else — no authenticator, no user provider, no firewall integration. Budget for writing the security glue yourself.
Bridge LightSAML into Symfony Security via a custom authenticator. The cleanest pattern is an AbstractAuthenticator on your ACS route:
public function authenticate(Request $request): Passport
{
$response = $this->build->getBindingFactory()->receive()->getResponse($httpRequest);
$this->validator->validate($response); // signature, destination, audience, timestamps
$assertion = $response->getFirstAssertion();
return new SelfValidatingPassport(
new UserBadge($this->extractNameId($assertion), [$this->userProvider, 'loadUserByIdentifier'])
);
}
Keep the parsing/validation in a dedicated service (SamlResponseHandler) so it is unit-testable without HTTP. On Symfony 6/7 the authenticator registration is security.firewalls.main.custom_authenticators (or just entry_point) — the doc block style differs from 5.4, so check the version of Symfony you're on before copying snippets from the upstream demo.
Convert Symfony\Component\HttpFoundation\Request to LightSaml\Model\Protocol\HttpRequest once, in a factory or a small kernel listener. LightSAML is transport-agnostic; every binding call needs the converted object. The upstream demo used HttpFoundationFactory/PsrHttpFactory; pick one and hide it behind a service so you can swap it.
Expose metadata with a controller, not a static file. Build the entity descriptor from lightsaml.system.own_entity_descriptor and serialize it with XmlWriter. That keeps the ACS URL and cert in sync with config, and IdPs re-fetch metadata on a schedule.
Attribute mapping: assertions arrive as LightSaml\Model\Assertion\Attribute objects; values are AttributeValue (sometimes with xsi:type, sometimes friendly names). Write a mapper class with an explicit table (mail → email, memberOf → roles) and log unknown attributes rather than guessing. Do not assume NameID is an email — it is often an opaque persistent identifier, so keep a separate saml_subject column on your user table for correlation.
Multi-IdP / multi-tenant: the container.build service is effectively a singleton per IdP. Use a service locator keyed by tenant, build a SystemContainer per tenant at runtime (or register one per IdP via a compiler pass), and pick the locator entry from the ACS URL or RelayState. The bundle config is not designed for N IdPs out of the box; plan for a custom SystemContainer factory.
Test with fixtures. Capture real SAMLResponse XML from a test IdP (Keycloak, SimpleSAMLphp, samltest.id), store as fixtures, and unit-test the handler — signature validation, expired assertions, wrong audience, tampered XML. Add a test that replays the same response twice and asserts the second is rejected (replay protection). Since 1.6.0 raised the PHP floor to 8.1, your test suite can assume PHPUnit 9.5+/10 and PHP 8 attributes — no need to keep 7.4-compatible test scaffolding around.
ResponseValidator, AssertionValidator), but they are not automatically invoked in the flows the demo shows. Explicitly check: XML signature (on response or assertion, per IdP config), Destination, Audience, NotBefore/NotOnOrAfter, InResponseTo against your stored request ID, and replay (one-time use of the assertion ID).InResponseTo and replay are the two most commonly skipped checks. Both need server-side state. The bundle exposes a request store service; use it (or Redis) rather than trusting the RelayState.SameSite=Strict, the user appears anonymous when the IdP posts back. Use Lax (works for top-level POST navigations) or a dedicated cookie on the ACS path. This costs people days.router.request_context and trusted_proxies so generateUrl(..., ABSOLUTE_URL) produces the public URL — otherwise Destination validation fails on a URL like http://internal-lb/....\n are a recurring headache — store as base64 and decode at boot.TimeProvider and make sure the app servers are NTP-synced. In tests, decorate the time provider rather than sleeping.LIBXML_NONET and modern libxml behavior. PHP 8.1+ (now mandatory) disables entity loading by default, so the XXE-era defaults of old lightsaml releases are less of a concern than they were — but do verify the lightsaml/lightsaml version resolved by Composer and don't run the parser against untrusted DTDs.cache:clear (or a container rebuild in prod deploy) means the old cert keeps signing. Bake this into your deploy script — and it is doubly important on the 1.5 → 1.6 upgrade, where the DI definitions themselves changed.bin/console debug:container lightsaml --show-private and your test suite with SYMFONY_DEPRECATIONS_HELPER=max[self]=0 to see whether the bundle's own definitions still trip Symfony 6.4 deprecations in your app; if they do, they'll become errors on 7.x.onelogin/php-saml, SimpleSAMLphp, or hand-wired LightSAML. You are not losing much by replacing it.lightsaml.system.own_entity_descriptor or the credential store in services.yaml; decorate the binding factory to add logging; provide a custom TimeProvider for tests; and intercept at the authenticator layer (not the bundle) for anything user-facing.How can I help you explore Laravel packages today?