lightsaml2/sp-bundle
Symfony bundle integrating LightSAML so your application acts as a SAML 2.0 service provider. Provides DI services, configuration, and routing to handle authn requests, assertions, and metadata exchange with identity providers.
The hard truth up front: lightsaml2/sp-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/sp_bundle.php that Laravel will publish. If you composer require it into a Laravel app you'll pull in symfony/http-kernel, symfony/dependency-injection, and a bundle bootstrap that never runs — it's dead weight. The asset you actually consume day-to-day is lightsaml/lightsaml, the framework-agnostic core the bundle wraps.
Treat the bundle as documentation for how to wire LightSAML correctly in a framework, not as a dependency.
Minimal steps to first login:
composer require lightsaml/lightsaml — do not require the bundle.DependencyInjection/Configuration.php — the canonical config schema (sp, idp, credentials, endpoints).Resources/config/services.yml — the exact LightSAML classes and parameters the bundle wires. This is your blueprint.app/Providers/SamlServiceProvider.php and bind a LightSaml\Store\EntityDescriptor\... + LightSaml\Credential\... graph built from config/saml.php.routes/web.php: /saml/metadata, /saml/login, /saml/acs, optionally /saml/logout./saml/login builds an AuthnRequest and redirects to the IdP; /saml/acs validates the SAMLResponse and calls Auth::login($user).Where to look in the core library: LightSaml\Builder\Profile\WebBrowserSso\Sp\SsoSpRedirectProfileBuilder — this is the whole login flow in one class.
Wire the core library into a service provider, not the bundle. Build the profile on demand in a controller (or bind it as a singleton) from your config:
// app/Providers/SamlServiceProvider.php
public function register(): void
{
$this->app->singleton(SsoSpRedirectProfileBuilder::class, function ($app) {
$sp = config('saml.sp'); // entityId, acsUrl, cert, key
$idp = config('saml.idp'); // entityId, ssoUrl, cert
$spCredential = (new X509Credential(
(new X509Certificate($sp['cert']))->loadPem(),
(new PrivateKey($sp['key']))->loadPem()
))->setEntityId($sp['entity_id'])->setUsage(CredentialUsage::Signing());
return new SsoSpRedirectProfileBuilder(
$sp['entity_id'],
$idp['sso_url'],
$sp['acs_url'],
/* store/context wiring per services.yml */
);
});
}
SP-initiated login controller — build message, produce redirect, don't hand-roll XML:
public function login(SsoSpRedirectProfileBuilder $builder)
{
$profile = $builder->build();
$httpRequest = $profile->getRequest(); // HttpRedirectBinding
$context = $profile->getAction()->createActionContext();
$context->getHttpRequest()->getQueryString(); // or use the message directly
return redirect()->away($profile->getRequest()->getBinding()->getMessageUrl(...));
}
In practice, use HttpRedirectBinding::send() or construct the redirect URL with SAMLRequest + RelayState query params yourself — the simplest reliable pattern is AuthnRequest → serialize → deflate/base64 → redirect. Keep RelayState to a short opaque token and store the intended destination server-side (session) to close open-redirect holes.
ACS controller — the security-critical endpoint:
public function acs(Request $request, SsoSpRedirectProfileBuilder $builder)
{
$response = $builder->getResponseContext()->getProtocolResponse(); // parse + validate signatures
// ^ core parses `SAMLResponse`, verifies XML-DSig against your configured IdP cert
/** @var Assertion $assertion */
$assertion = $response->getFirstAssertion();
$this->assertNotExpired($assertion);
$this->assertAudience($assertion, config('saml.sp.entity_id'));
$this->assertInResponseTo($assertion, session('saml_request_id'));
$attrs = parse_attributes($assertion);
$user = User::firstOrCreate(
['email' => $attrs['mail']],
['name' => $attrs['displayName'] ?? $attrs['mail']]
);
Auth::login($user);
$request->session()->regenerate();
event(new SamlLogin($user, $attrs));
return redirect()->intended('/dashboard');
}
IdP-initiated is the same ACS, minus the login route and InResponseTo check. You still validate audience, timestamps, and signature. Never skip signature validation for "IdP-initiated is trusted" — that's how wrapping attacks land.
Metadata endpoint — generate from EntityDescriptor + X509Certificate:
public function metadata()
{
$ed = new EntityDescriptor($this->spEntityId);
$ed->addSpSsoDescriptor((new SpSsoDescriptor())
->addSingleSignOnService(SingleSignOnService::newRedirect($this->acsUrl))
->addKeyDescriptor(KeyDescriptor::newSigning(X509Certificate::load($spCert))));
return response($ed->serializeToXml(), 200, ['Content-Type' => 'application/samlmetadata+xml']);
}
Cache the string — certificates change rarely, and every login shouldn't re-serialize metadata.
User provisioning lives in your ACS controller (or a listener on the SamlLogin event), not in the bundle. That's the "pluggable user provisioning hook" the description refers to — you're the hook.
Firewall/middleware: don't try to bolt SAML into Laravel's auth guard. Use a route middleware EnsureSamlAuthenticated that checks Auth::check() plus a session flag set in ACS, and route to /saml/login otherwise. Keeps the SP flow completely outside Laravel's guard machinery.
composer require lightsaml2/sp-bundle in a Laravel app will either fail resolution (Symfony 6/7 vs. what Laravel expects) or drag in a bundle that never boots. Verify with composer why-not before fighting it. The services.yml is the deliverable, not the dependency.SAMLResponse is POST form-encoded and often base64-inflated. Laravel's $request->input('SAMLResponse') is fine, but do not run it through csrf — that route must be VerifyCsrfToken::except(['saml/acs']) in app/Http/Middleware/VerifyCsrfToken.php.NotBefore/NotOnOrAfter. Allow ~2 minutes of skew; if you must, override the assertion timestamp validator, but prefer NTP on the boxes. Most "works in stage, fails in prod" SAML bugs are clock or certificate expiry.config/saml.php, and add a health check / scheduler task that alerts before the cert expires. Nothing else in the bundle does this for you.config/saml.php with sp.entity_id, sp.acs_url, sp.cert, sp.key, idp.entity_id, idp.sso_url, idp.cert. Load keys from env('SAML_SP_KEY') with \n escaping handled — multiline PEM in .env is a common footgun; prefer paths via storage_path('saml/sp.key') and file_get_contents.LightSaml\Store\Id\IdStoreInterface (replay protection — back it with Redis or a DB table), LightSaml\Store\Request\RequestStateStore (for InResponseTo validation — back it with the session), and the assertion validators chain. Implement these in Laravel idioms; the bundle's implementations are Symfony-service-shaped and you don't want them.$response->getSignature(), $assertion->getSignature(), and the canonicalized signed XML string. 90% of failures are whitespace/canonicalization (C14N) or a mismatched certificate. openssl x509 -in idp.pem -noout -dates first.lightsaml/lightsaml to an exact version, run composer audit in CI, and have a fork plan. If this is going into a customer-facing auth path, budget for a security review and expect to be the maintainer of last resort.How can I help you explore Laravel packages today?