lightsaml2/symfony-bridge
Symfony bridge for LightSAML2 that integrates SAML 2.0 service provider and identity provider support into Symfony apps, with bundle configuration, security firewall integration, and endpoints for authentication requests, responses, metadata, and single logout.
First: establish what this package is before you composer require anything. lightsaml2/symfony-bridge is a Symfony bundle — it registers compiler passes, firewall factories, and DI services for Symfony\Bundle\SecurityBundle. It has no ServiceProvider, no Guard/UserProvider, no config/*.php merge pipeline. In a Laravel app you cannot install it as a dependency. The realistic day-to-day use is as a reference implementation you read and port, or as a probe you run in a throwaway Symfony skeleton to see the correct shape of an SP before hand-rolling the Laravel side.
Where to look first, in this order
DependencyInjection/Configuration.php — the config tree. This is the single most valuable file in the repo. It tells you what an SP actually needs: sp.entity_id, sp.assertion_consumer_service, sp.single_logout_service, sp.private_key/certificate, per-IdP metadata/certificate, and attribute_mapping. Copy this schema into config/saml.php.Resources/config/services.yml (or .xml) — the wiring list. Enumerate the services it constructs; each one is a class you will need a Laravel equivalent for.Security/Factory/*Factory.php) — the SamlProvider, SamlListener, and token classes. This is the part that does not port; it is the piece you replace with a Laravel guard.Minimal probe to see it work (Symfony, throwaway app)
# config/packages/security.yaml
security:
providers:
saml:
id: saml_user_provider # your custom provider
firewalls:
main:
saml:
provider: saml
sp:
entity_id: 'https://app.test/saml/metadata'
assertion_consumer_service: 'https://app.test/saml/acs'
idp:
entity_id: 'urn:test:idp'
metadata: '/path/to/idp-metadata.xml'
// config/bundles.php
return [ LightSaml\SymfonyBridgeBundle\LightSamlSymfonyBridgeBundle::class => ['all' => true] ];
Hit /saml/metadata, paste it into your IdP (Okta dev tenant or a local simplesamlphp instance), attempt SP-initiated login, and confirm an assertion round-trips. That is the whole first use case — prove metadata exchange and assertion parsing end to end on a test IdP before you design anything.
Then translate into Laravel — the actual build
// routes/web.php — three routes, nothing more
Route::get('/saml/metadata', [SamlController::class, 'metadata']);
Route::post('/saml/acs', [SamlController::class, 'acs'])->name('saml.acs');
Route::get('/saml/sls', [SamlController::class, 'sls']); // defer initially
// config/saml.php — derived from the bundle's Configuration.php
return [
'sp' => [
'entity_id' => env('SAML_SP_ENTITY_ID'),
'acs' => route('saml.acs'),
'private_key' => storage_path('saml/sp.key'),
'certificate' => storage_path('saml/sp.crt'),
],
'idp' => [
'entity_id' => env('SAML_IDP_ENTITY_ID'),
'sso_url' => env('SAML_IDP_SSO_URL'),
'certificate' => env('SAML_IDP_CERT'),
],
'attribute_map' => ['email' => 'mail', 'name' => 'displayName', 'role' => 'memberOf'],
];
The underlying engine to depend on is lightsaml/lightsaml (the core library), not this bundle. Budget 2–4 weeks for the guard + ACS + metadata + JIT provisioning if you have not done SAML before. Day one is still composer require --dry-run lightsaml2/symfony-bridge in a scratch branch to confirm the PHP/Symfony constraints — expect it to fail on Laravel 11/12 + PHP 8.3, and let that end the dependency question cleanly.
Pattern 1 — Bundle as a spec, not a dependency. The productive workflow is: read the bundle, extract the three artifacts that carry real value (metadata XML generation, AuthnRequest construction, response-validation orchestration), and re-express them as a Laravel service + route group + guard. Treat every file under Security/ as pseudocode for the guard you are about to write.
Pattern 2 — Config-schema-first porting. Port Configuration.php into a config/saml.php with the same key names. You get three things for free: (a) a config file your ops team can reason about, (b) a natural place for per-tenant IdP blocks later, (c) a diff-friendly surface when you compare your implementation against the bundle and against a maintained alternative (nbgrp/oneloginsaml-bundle, hslavich/OneloginSamlBundle) if you decide to switch engines.
Pattern 3 — Guard + provider, mirroring the firewall. The bundle's SamlProvider becomes a Laravel guard; its user provider becomes your UserProvider. Keep them separate so the guard handles assertion → identity and the provider handles identity → user record.
class SamlGuard implements Guard
{
public function user()
{
$assertion = $this->validator->validate($this->request->input('SAMLResponse'));
$identity = $this->mapAttributes($assertion); // email, name, groups
$user = $this->provider->retrieveBySamlIdentity($identity); // JIT here
return $user; // null on failure -> guard denies
}
}
Pattern 4 — JIT provisioning in the provider, not the guard. Attribute mapping and user creation are business logic. Emit a SamlUserProvisioned event from the provider so downstream listeners (role sync, welcome email, audit log) do not get baked into auth code.
Pattern 5 — Additive rollout, feature-flagged per tenant. Do not replace your existing session login. Register SAML as an additional guard alongside it, gate it with a per-tenant flag in config, and only promote it to primary once you have run a full login cycle against every in-scope IdP. This mirrors how the bundle's firewall sits next to other authentication listeners in Symfony.
// config/auth.php
'guards' => [
'web' => ['driver' => 'session', 'provider' => 'users'],
'saml' => ['driver' => 'saml', 'provider' => 'users'],
],
Pattern 6 — RelayState carries the intended destination. On SP-initiated login, stash url()->intended() in a signed cookie or session keyed before redirecting to the IdP, and restore it in the ACS handler. Without this every login lands on /.
Pattern 7 — If you have a genuinely mixed Symfony/Laravel estate. Run the bundle in the Symfony service (where it belongs), and have Laravel trust the resulting session/SSO token. Do not attempt to bridge the firewall listener into Laravel — that integration seam does not exist.
lightsaml2/* is actively maintained versus a namespace-squat on an abandoned original. Check last release date, open issues, and whether the bundle references Symfony\Component\Security\Core\Authentication\Token\* classes that were removed in Symfony 6 — that one class-hierarchy change is a common reason these bridges stop loading.NotOnOrAfter/NotBefore failures by region — they surface as random login failures that reproduce nowhere.SAMLResponse and pretty-print the XML; inspect with xmlsec1 --verify; use a browser SAML tracer extension to capture the real POST; compare Audience and Recipient against your actual SP entity ID and ACS URL — a trailing slash mismatch is the single most common cause of "it worked yesterday."RelayState will fail in ways that do not reproduce locally. Test through the real ingress.How can I help you explore Laravel packages today?