Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Lightsaml Laravel Package

litesaml/lightsaml

Lite Saml is a low-level SAML 2.0 library for PHP: data model classes, XML serialization, security and certificate support, and message bindings to build SP and IdP integrations.

View on GitHub
Deep Wiki
Context7

Getting Started

Install:

composer require litesaml/lightsaml:^6.1

Version pinning matters: 5.0.0+ requires PHP 8.4 and 5.0.1 contains the XML Signature Wrapping (XSW) fix. If you are on PHP 8.2/8.3 you will be stuck on 4.7.x, which is vulnerable to XSW — that is an unacceptable trade for a user-facing login path, so plan the PHP upgrade rather than downgrading.

Read first, in this order:

  1. The Cookbook at https://litesaml.github.io/cookbook/ — it is the only real documentation beyond the source.
  2. The model namespaces, which mirror the SAML spec almost 1:1: LightSaml\Model\Protocol\* (AuthnRequest, Response, LogoutRequest/Response), LightSaml\Model\Assertion\* (Assertion, Subject, Conditions, AttributeStatement), LightSaml\Model\Metadata\* (EntityDescriptor, IdpSsoDescriptor, SpSsoDescriptor, KeyDescriptor).
  3. LightSaml\Binding\BindingFactory, LightSaml\Credential\X509Certificate, LightSaml\Store\*.

Know what you are buying. This is not a middleware package. There is no "protect this route" facade. It is a data-model + XML-security toolkit, so the flow (build → redirect → receive → verify → map to user) is yours to own. If you want day-to-day convenience, layer litesaml/saml (ServiceProviderWrapper / IdentityProviderWrapper) on top, or use this package to build your own thin service class once and stop thinking about it.

First use case (SP-initiated login), end to end:

use LightSaml\Model\Protocol\AuthnRequest;
use LightSaml\Model\Assertion\Issuer;
use LightSaml\Model\Protocol\NameIDPolicy;
use LightSaml\Binding\BindingFactory;
use LightSaml\SamlConstants;
use LightSaml\Helper;

$authn = (new AuthnRequest())
    ->setID(Helper::generateID())
    ->setIssueInstant(new \DateTime())
    ->setDestination(config('saml.idp.sso_url'))
    ->setAssertionConsumerServiceURL(route('saml.acs'))
    ->setProtocolBinding(SamlConstants::BINDING_SAML2_HTTP_POST)
    ->setIssuer(new Issuer(config('saml.sp.entity_id')))
    ->setNameIDPolicy(new NameIDPolicy('urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', true));

// persist $authn->getID() in the session — you will need it to check InResponseTo
$psrResponse = (new BindingFactory())
    ->create(SamlConstants::BINDING_SAML2_HTTP_REDIRECT)
    ->send($authn);

return response('', 302, ['Location' => $psrResponse->getHeaderLine('Location')]);

Implementation Patterns

Bridge PSR-7 into Laravel once. Since 5.0 the bindings speak PSR-7 instead of Symfony HttpFoundation. Add symfony/psr-http-message-bridge + nyholm/psr7 and wrap the incoming request in a small helper (or a middleware that stashes a ServerRequestInterface on the container) so controllers don't repeat the conversion.

Receive with getBindingByRequest(), not by guessing the binding:

$factory = new BindingFactory();
$message = $factory->getBindingByRequest($psrRequest)->receive($psrRequest);
// returns a SamlMessage (Response / AuthnRequest / LogoutRequest) since 4.7

Note the redirect binding handles compressed and uncompressed payloads since 4.4 — do not write your own inflate step.

Verification pipeline — treat it as a checklist, because the library will not do it for you. After deserializing a Response:

  1. $response->getSignature()->validate($idpPublicKey) (or $response->getAssertions()[0]->getSignature()->validate(...) if the IdP signs at assertion level — support both).
  2. $response->getInResponseTo() === session('saml.request_id'), then forget that key.
  3. $response->getDestination() matches your ACS URL.
  4. $assertion->getSubject()->getSubjectConfirmation()->validate(...) for NotBefore / NotOnOrAfter / Recipient, with a small clock-skew tolerance (~2 min).
  5. $assertion->getConditions()->getNotBefore()/getNotOnOrAfter() and every AudienceRestriction contains your SP entity ID.
  6. Replay: assertion ID must not be in your Id store.

Deserialize manually when you need control (e.g. for tests or for a stored SAMLResponse):

$doc = new \DOMDocument();
$doc->loadXML($xml, LIBXML_NONET);
$response = \LightSaml\Model\Protocol\Response::fromXML($doc->documentElement);

Use the store abstractions rather than hand-rolling state:

  • LightSaml\Store\Request\RequestStateSessionStore — correlates outbound requests with inbound responses. Map it onto Laravel's session, or implement RequestStateStore over the session/cache.
  • LightSaml\Store\Id\CacheIdStore — replay protection. Wire it to a cache repository with a TTL of at least the assertion lifetime.
  • LightSaml\Store\Credential\X509FileCredentialStore($entityId, $certPath, $privateKeyPath) — resolves signing/verification credentials by entity ID. For rotating or per-tenant IdPs, implement CredentialStore yourself and read from config/DB with a cache.

Metadata is a model, not a fetcher. Download the IdP metadata XML on a schedule (a queued job), parse into EntityDescriptor::fromXML(), then reach into getIDPSSODescriptor() for the SingleSignOnService URL and getKeyDescriptors() for certificates. Cache the parsed values — certs change, so refetch on failure.

Encrypted assertions: if the IdP encrypts, unwrap with LightSaml\Model\XmlDSig\EncryptedElementReader using your SP private key before validating signatures. Keep the decryption key in the same credential store as the signing key so rotation is one config change.

Multi-tenant / multi-IdP: key everything by entity ID. One CredentialStore, one metadata cache entry, and one saml_idp column on your tenant table. Do not fork the flow per tenant.

Gotchas and Tips

Signature validation scope is the #1 footgun. Validating the Response signature alone is insufficient when the IdP signs the Assertion (and vice versa). Validate whatever is signed, and re-verify that the assertion you extracted belongs to the signed response — the 5.0.1 XSW fix enforces this internally, but only if you are on ≥ 5.0.1. Check your lockfile.

The library never checks InResponseTo, Destination, Audience, or Recipient. These are exactly the checks that stop response-injection and cross-SP replay. A green validate() call means "the XML signature is cryptographically sound", not "this login is legitimate."

Replay protection requires a store. Without an IdStore, a captured assertion can be replayed until it expires. Use the cache-backed store and make sure the TTL outlives NotOnOrAfter.

Clock skew causes the worst support tickets. IdPs and PHP hosts drift; validateNotBefore is strict. Apply a tolerance constant and log the raw timestamps when rejecting.

Certificate quirks in 6.1.0:

  • Non-RSA public keys now throw Unsupported certificate public key type — this includes EC certs issued by an RSA CA. If a partner hands you an EC cert you must stay on an older version or negotiate RSA.
  • Certificates issued with SHA-1 (or MD5, or by an ECDSA CA) now sign with rsa-sha256 instead of inheriting rsa-sha1. That is the compliance win, but it will break a partner IdP that demands rsa-sha1 — which is a signal to retire that partner, not to pin an old version.
  • RSA-PSS is supported since 4.7 if a partner requires it.

Default digest is SHA-256 since 4.7. If you are comparing your outbound XML against a partner's expected fixture, the change is intentional and not a bug.

Schema validation is opt-in and needs a second package. The XSDs moved to a dedicated package at 3.0 (litesaml/schema). In production, prefer full signature + semantic checks over XSD validation; use XSD only in tests or when debugging malformed partner XML.

Deflate/redirect ordering: sign first, then deflate + base64 + urlencode. Building the redirect URL by hand almost always gets this wrong; let HttpRedirectBinding do it.

Debugging: log the raw base64 payload before deserialization (Storage::put behind a feature flag in staging), and log $message->getIssuer()->getValue() plus the assertion ID on every failure. LightSamlBindingException (thrown instead of a TypeError since 5.1) means the request did not look like SAML at all — usually the PSR-7 conversion dropped the POST body.

Extension points to reach for before forking: custom CredentialStore / IdStore / RequestStateStore implementations, a custom SignatureValidator if you need key rollover across multiple certificates, and the model classes' serialize() hook for adding vendor-specific attributes (AttributeStatement values are just strings — map them to Eloquent users in one dedicated mapper class, not inline in the controller).

Keep a single SamlService class. Given how much of this flow is manual, the real risk is duplication: three controllers each with their own half-correct validation order. One service, one checklist, unit-tested with fixture XML from the IdP.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource