mlightsaml/lightsaml
LightSAML is a PHP SAML 2.0 library with message models, bindings, metadata, and XML signing/encryption support. It helps build service provider and identity provider integrations, including in Laravel apps.
mlightsaml/lightsaml is a protocol library, not a Laravel package. There is no service provider, no config file, no facade, and no HTTP middleware. You composer require mlightsaml/lightsaml and then wire it into your own services. Plan on writing 300–600 lines of glue code for a working SP flow.
Note the repo you linked (DiegoCardosoSenaiSC/lightSAML, 0 stars, 0 dependents) is a fork of the upstream mlightsaml library. Before committing, diff it against lightsaml/lightsaml and mlightsaml/lightsaml on Packagist, pin the exact version in composer.json, and treat the fork as a supply-chain risk if it is not actively maintained.
Where to look first, in order:
LightSAML\Core\Model — AuthnRequest, Response, Assertion, Subject, NameID, Issuer. This is the object model you will actually touch daily.LightSAML\Core\Binding — HTTP-Redirect and HTTP-POST binding classes. These handle Base64, DEFLATE, and URL encoding for you. Do not reimplement these.LightSAML\Core\Profiles\WebSso\Sp — the SpSsoProfile (and IdpSsoProfile for the reverse direction). This is the closest thing to a "flow runner" in the library.LightSAML\Core\Context — ProfileContext, CredentialContext, EndpointContext. Everything is passed around via these DTOs, which is why the library feels verbose.LightSAML\Core\Store — IdStore, RequestStore, CredentialStore. You will need these for replay protection and to remember the AuthnRequest ID.First use case — SP-initiated login:
// 1. Build the request
$authn = new AuthnRequest();
$authn->setId($requestId = (string) Str::uuid());
$authn->setAssertionConsumerServiceURL(route('saml.acs'));
$authn->setIssuer(new Issuer(config('saml.sp.entity_id')));
$authn->setDestination(config('saml.idp.sso_url'));
// 2. Persist the request ID (cache, 10 min TTL) for InResponseTo checking
Cache::put("saml.req.{$requestId}", true, now()->addMinutes(10));
// 3. Serialize, sign, and hand to the HTTP-Redirect binding
// (use the binding class; it does deflate + base64 + urlencode)
return redirect()->away($binding->createRedirectUrl($authn, $relayState));
The ACS endpoint is the inverse: read SAMLResponse from the POST body, bind it with the HTTP-POST binding, verify the signature against the IdP certificate, then validate Destination, Audience, NotBefore/NotOnOrAfter, and InResponseTo.
If your IdP supports it, start with IdP-initiated in local dev because it removes the request/RelayState round-trip and gets you a signed assertion faster.
Wrap the library in one service class. Bind an SamlService (or separate SpService) into Laravel's container and inject configuration. Never call the library from controllers or middleware directly — its API surface is wide and you want a single seam to swap it or mock it.
Build a config array early. The library is credential- and endpoint-oriented; assemble this once:
sp.entity_id, sp.acs_url, sp.slo_url, sp.key_path, sp.cert_pathidp.entity_id, idp.sso_url, idp.slo_url, idp.cert_path (support an array of certs for rotation)Credential objects, not raw PEM strings. Use X509Certificate and X509Key, put them in a CredentialContext, and load them from storage/app/saml/ (or a secrets manager). Never commit them; never store private keys in the database unencrypted.
Store only what you need in the session. The canonical pattern:
AuthnRequest ID → RelayState mapping.InResponseTo matches a cached ID; delete the cache entry (replay protection).NameID and attributes into a plain array.Auth::login($user) and $request->session()->regenerate() to prevent fixation.Attribute mapping as a config map. Rather than hardcoding eduPersonPrincipalName, define:
'mapping' => [
'email' => ['urn:oid:0.9.2342.19200300.100.1.3', 'mail'],
'name' => ['urn:oid:2.5.4.42', 'displayName'],
],
Handle both friendly names and OIDs; IdPs differ wildly on this.
Multi-tenant / multiple IdPs. Because the library is stateless, the cleanest pattern is one guard per tenant, with the IdP config resolved from the tenant record. Key your AuthnRequest cache by tenant + request ID so an ACS endpoint can look up the right IdP certificate before verifying. You cannot verify a signature until you know which IdP sent it, so the request ID lookup must happen before signature validation.
Metadata. Generate your SP metadata from the model classes and serve it at a stable URL (/saml/metadata). Cache the IdP metadata XML but refresh daily and re-fetch on unknown signing cert — IdPs rotate certificates.
Testing. Snapshot real SAMLResponse payloads from a local IdP (SimpleSAMLphp or Keycloak) into tests/Fixtures/. Assert: valid response succeeds, tampered XML fails, wrong audience fails, expired NotOnOrAfter fails, replayed ID fails. Disable verification only via an explicit testing flag — never automatically in local, or it will sneak into staging.
Logout. SLO is more painful than login. Most teams ship SP-initiated logout as "clear local session, then redirect to IdP's SLO URL," and skip IdP-initiated logout until a customer complains.
gzdeflate), not gzencode/gzip. This is the #1 homegrown bug. Use the bundled binding class.DOMDocument once with preserveWhiteSpace = true and keep it intact.SAMLResponse POST field is Base64-encoded; the Redirect query param is deflate + base64 + URL-encoded.Y-m-d\TH:i:s\Z. PHP's date() respects your server timezone; set date_default_timezone_set('UTC') or use gmdate. Add ~60–120 s of clock skew tolerance to NotBefore/NotOnOrAfter.Audience equals your SP entity ID, Destination equals your ACS URL, and Recipient matches. Many implementations forget this and are trivially replayable.//ds:Signature XPath and then trust //saml:Subject elsewhere in the document. Verify the signed element is the exact DOM node you extract data from. The library handles this correctly only if you use its validation path consistently.InResponseTo is mandatory for SP-initiated flows and absent in IdP-initiated flows. Track which kind of flow you're handling. If the assertion has no InResponseTo, treat it as higher risk (and consider requiring RelayState to match).Cache::put("saml.asserted.{$assertionId}", true, ...)) to reject replays, distinct from request ID checks.LightSAML\Core\Exception. Catch broad ValidationException and similar, log the technical detail server-side, and show the user a generic error page. Returning the XML or exception message to the browser leaks your IdP configuration and assertion contents.SAMLResponse at info or error — it contains PII and a bearer-like assertion. Redact it or gate it behind a saml.debug config flag.sha256) — SHA-1 is rejected by most modern IdPs.$_SERVER['HTTP_HOST'] may not match your configured ACS URL. Generate URLs with url()/route() and configure TrustProxies, or the Destination check will fail intermittently.array cache will fail across requests. Use Redis or database cache in production; file cache is fine for single-node.lightsaml/lightsaml) for security fixes. If you just need SP login, evaluate onelogin/php-saml or simplesamlphp/simplesamlphp first — they are more actively maintained and better documented, even if less "object-oriented."How can I help you explore Laravel packages today?