mlightsaml/sp-bundle
Symfony bundle that adds SAML 2.0 Service Provider support using LightSAML. Configure identity providers, enable SSO login and logout, generate and consume metadata, and integrate SAML authentication into Symfony applications through flexible bundle configuration.
Read this first: mlightsaml/sp-bundle is a Symfony bundle, not a Laravel package. It ships a Bundle class, a DI extension, a config tree, and services bound to Symfony's HttpKernel. It will not boot under Laravel's container. If you're a Laravel developer, the decision is made at the top: do not composer require this into a Laravel app. The reusable asset is the upstream lightsaml/core library (XML-DSig, AuthnRequest/Response models, metadata parsing), not the wrapper. Use this repo as a reference implementation for wiring, then build the SP against lightsaml/core directly (or start from slab/saml2).
If you're actually on Symfony — or doing a throwaway spike — the first 10 minutes are an audit, not an install:
composer.json before anything else. Look for: the symfony/* version constraints (forks of this bundle historically pin ^3.4|^4.0), the lightsaml/lightsaml version it pulls in (expect a PHP 7-era pin), and whether it declares replace: lightsaml/sp-bundle. Without the replace declaration, installing this alongside upstream gives you duplicate service definitions and opaque DI errors.composer config repositories.sp-bundle vcs https://github.com/DiegoCardosoSenaiSC/SpBundle
composer require mlightsaml/sp-bundle
// config/bundles.php
return [
// ...
LightSaml\SpBundle\LightSamlSpBundle::class => ['all' => true],
];
# config/packages/light_saml_sp.yaml
light_saml_sp:
entity_id: "https://app.example.com/saml/metadata"
credentials:
- id: main
certificate: "%kernel.project_dir%/config/saml/sp.crt"
private_key: "%kernel.project_dir%/config/saml/sp.key"
idp:
entity_id: "https://idp.example.com/metadata"
single_sign_on_service: "https://idp.example.com/sso"
single_logout_service: "https://idp.example.com/slo"
x509cert: "%kernel.project_dir%/config/saml/idp.crt"
bin/console debug:router | grep -i saml), fetch it, and paste the XML into a test IdP (SimpleSAMLphp or a vendor sandbox). Set the ACS URL the bundle advertises, then exercise SP-initiated login. If metadata and ACS URLs don't round-trip cleanly, nothing downstream will.Where to look in the source, in order: the DI extension (DependencyInjection/), the config tree, the metadata/AuthnRequest factory services, and the Response processor. That tells you the shape you'll reimplement — in Symfony you can just consume the services; in Laravel you re-derive them from lightsaml/core.
The core flow (framework-agnostic — this is what you actually port):
AuthnRequest from the SP entity, the IdP SingleSignOnService, and a fresh request ID.RelayState must survive the redirect to the IdP and back. Don't use local file sessions. Use Redis or a signed cookie, keyed by request ID, with an expiry slightly longer than your IdP's expected round-trip. This is the single most common production failure in multi-node deployments.SAMLResponse, looks up the in-flight request state by ID, validates the response, and only then establishes the session.Symfony shape: the bundle exposes the SP entity, AuthnRequest factory, and Response processor as container services. Your controller resolves the SP, calls the factory, and redirects; the ACS controller resolves the processor, validates, and calls your security layer's login. You're writing controllers, not protocol code.
Laravel shape (the realistic path): depend on lightsaml/core and write the equivalent yourself. Routes:
Route::get('/saml/login', [SamlController::class, 'login']);
Route::post('/saml/acs', [SamlController::class, 'acs'])->withoutMiddleware([VerifyCsrfToken::class]);
Route::get('/saml/metadata', [SamlController::class, 'metadata']);
Route::get('/saml/logout', [SamlController::class, 'logout']);
The ACS handler is the only place that matters for security: parse → validate signature → validate Destination/Audience/InResponseTo → check NotBefore/NotOnOrAfter with explicit leeway → check the assertion ID against your replay cache → map attributes → Auth::login().
Attribute mapping. SAML carries identity as name/value pairs (mail, uid, groups). Decide your mapping before writing the controller: which attribute is the stable identifier, which is display name, which drive authorization. Do not silently fall back to a missing attribute — a missing uid should be a hard 401 with a logged reason, not an anonymous session.
Metadata endpoint. Generate once from the SP entity + credentials, serve as application/samlmetadata+xml, and let the IdP pull it (or hand it over). Keep it deterministic — same bytes every request — so you can diff it across deploys and so IdP re-fetches don't churn.
Multi-IdP (1–3 tenants max for this pattern). Key your trust store and credentials per tenant: {tenant_id: {entityId, ssoUrl, x509cert, ...}}. Tenant ID comes from the URL path or the login form, never from the unvalidated assertion. Each tenant gets its own metadata document (distinct entityID).
Testing against a real IdP. Stand up SimpleSAMLphp as a local IdP in the throwaway branch. You need it to prove: signature validation, audience/destination mismatch rejection, NotOnOrAfter expiry, and replay. A mocked response object proves none of that.
IdP-initiated (only if forced). Assertion-only flows skip InResponseTo validation because there's no in-flight request to match — this is exactly where signature-wrapping risk concentrates. If the IdP demands it, add strict Destination == your ACS and Audience == your entity ID checks, and consider it a distinct, higher-risk code path.
composer step and fails everywhere after — you end up hand-booting a Symfony container or writing the SP against lightsaml/core. Pick the latter and skip the dependency.Destination/Audience, and rejecting duplicate/extra assertions.How can I help you explore Laravel packages today?