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

Saml Bundle Laravel Package

pdias/saml-bundle

Symfony bundle for adding SAML 2.0 single sign-on to PHP applications. It integrates SAML toolkit configuration for service providers and identity providers, helping handle SP-initiated login, assertions, and authentication flows in Symfony.

View on GitHub
Deep Wiki
Context7

Getting Started

Check the version you're installing before anything else. v2.0 is the release that adds Symfony 4 support — the previous line targeted Symfony 3. So on Symfony 4 you must be on ^2.0; if Composer resolves you to 1.x you'll get a bundle that wires services using Symfony 3 conventions and fails at container compile time. Pin explicitly rather than trusting require to pick:

composer require pdias/saml-bundle:^2.0

Understand the scope of the release. "Support symfony 4" is the entire changelog. There are no new features, no new configuration keys, no deprecations announced. That's good news (your knowledge of the settings tree carries over) but it's still a major version bump, so treat the upgrade as "verify, don't assume": diff DependencyInjection/Configuration.php and Resources/config/services.yml between the tag you were on and v2.0. Those two files are the entire surface area of this bundle and they're short.

Reality check on the package itself. It remains a thin settings container around onelogin/php-saml 3.x, which predates PHP 8. Expect to loosen that constraint to ^4.0 on PHP 8.1+ and to tolerate deprecation noise from xmlseclibs. Symfony 5/6 is not claimed by this release; the bundle may well still work (it uses little of the framework), but validate with cache:clear and debug:container before committing to it.

Register manually — there is no Flex recipe.

// config/bundles.php
return [
    // ...
    Pdias\SamlBundle\SamlBundle::class => ['all' => true],
];

Confirm the exact FQCN and namespace casing in the repo root before copying — an unregistered or mis-cased bundle fails at cache:clear with a class-not-found, which is a confusing first error.

Configure the SP and IdP. The configuration tree mirrors php-saml's settings array one-to-one:

# config/packages/saml.yaml
saml:
    settings:
        strict: true
        debug: false
        baseurl: "%env(SAML_SP_BASEURL)%"
        sp:
            entityId: "%env(SAML_SP_ENTITY_ID)%"
            assertionConsumerService:
                url: "%env(SAML_SP_ACS_URL)%"
            singleLogoutService:
                url: "%env(SAML_SP_SLO_URL)%"
            x509cert: "%env(SAML_SP_CERT)%"
            privateKey: "%env(SAML_SP_KEY)%"
        idp:
            entityId: "%env(SAML_IDP_ENTITY_ID)%"
            singleSignOnService:
                url: "%env(SAML_IDP_SSO_URL)%"
            singleLogoutService:
                url: "%env(SAML_IDP_SLO_URL)%"
            x509cert: "%env(SAML_IDP_CERT)%"
        security:
            wantAssertionsSigned: true
            authnRequestsSigned: true
            requestedAuthnContext: false

If your version exposes a flattened saml.sp.entity_id-style tree instead, the keys still map directly to the php-saml docs — DependencyInjection/Configuration.php in the bundle is the ground truth, and it is short enough to read in full. Do this on upgrade: it's the single most likely place for a surprise in a major bump.

First use case: SP-initiated login. The auth service is what you'll inject everywhere. It's defined in Resources/config/services.yml; find the alias with:

bin/console debug:container | grep -i saml

On Symfony 4 the bundle's services may not be autowirable out of the box (see Gotchas), so if the alias isn't there, you'll define it yourself — that's a two-line fix, not a blocker. Then:

// src/Controller/SamlController.php
use OneLogin\Saml2\Auth; // or the bundle's wrapper service

public function login(Auth $auth): Response
{
    $auth->login(); // sends an HTTP redirect to the IdP
}

public function acs(Auth $auth): Response
{
    $auth->processResponse();

    if (!$auth->isAuthenticated()) {
        throw new \RuntimeException(implode(', ', $auth->getErrors()));
    }

    $nameId = $auth->getNameId();
    $attributes = $auth->getAttributes(); // ['mail' => ['[email protected]'], ...]
}

That redirect-and-callback pair is 90% of real-world usage. Get it working against samltest.id or a local SimpleSAMLphp IdP before wiring it into security.yaml.

Implementation Patterns

Bridging to Symfony security. Don't fight the bundle's lack of native authentication — wrap it in a Guard authenticator (Symfony 4.4) or a custom AuthenticatorInterface (5.1+, if you've validated the bundle there). The pattern is: supports() returns true for your ACS route, authenticate() calls $auth->processResponse(), getCredentials()/getUser() pulls the NameID plus mapped attributes, and createAuthenticatedToken() returns a token whose user was just-in-time-provisioned or loaded by email from the attribute map. Keep the redirect-out flow in a separate controller action; authenticators are awkward for initiating SSO.

Inject settings, not the wrapper, where you can. Because the bundle is essentially a DI-wired settings array, a small SamlSettingsFactory service that reads your config and returns the php-saml array gives you far more leverage than the bundle's own service — and survives the next major bump.

Attribute mapping belongs in your app, not the bundle. The bundle hands you a raw getAttributes() array where every value is a list (['mail' => ['[email protected]']]). Write one small mapper class so you're not indexing [0] in five places, and decide early whether identity is NameID or an attribute (e.g. urn:oid:0.9.2342.19200300.100.1.3 for mail) — IdPs differ and some send both.

Expose SP metadata as a route. Register an unauthenticated controller that returns the generated XML and hand that URL to your IdP admin:

$metadata = $auth->getSettings()->getSPMetadata();
return new Response($metadata, 200, ['Content-Type' => 'application/xml']);

This is the cheapest way to keep ACS URLs, entityID and certificates in sync with what the IdP has stored.

Single logout. $auth->logout() builds a signed LogoutRequest and redirects to the IdP SLO endpoint; the SLO handler calls processSLO(). If the IdP doesn't support SLO (common), implement local-only logout and accept that IdP sessions linger — don't block the feature on it.

Session-based identity. Most implementations stash the NameID/attributes in the session immediately after ACS and let Symfony's normal session-based firewall take over from there. That keeps SAML out of the request path for every subsequent request, which is what you want — processResponse() is not cheap.

Multi-IdP. If you need more than one IdP, the bundle's single static settings tree gets in the way. Build the settings array yourself at runtime (inject a settings provider, instantiate new Auth($settings) per tenant) rather than extending the bundle's DI config.

Gotchas and Tips

Symfony 4 made services private — this is the v2.0 upgrade trap. Since Symfony 4, bundle services are private and not autowired by default. If debug:container | grep -i saml shows the service but injecting OneLogin\Saml2\Auth throws "cannot autowire", add an alias in your app config:

# config/services.yaml
services:
    OneLogin\Saml2\Auth:
        alias: saml.auth   # confirm the real id with debug:container
        public: true

The bundle's own services.yml may not have been updated for Symfony 4 idioms (it likely still lives under Resources/config/), so expect to do this wiring yourself. It's not a defect — it's the cost of a ten-line bundle.

Service file location. The bundle loads Resources/config/services.yml. That path still works on Symfony 4/5, but if you've adopted the config/services.yaml convention everywhere else, don't be confused when the bundle's own file isn't in that directory. Check DependencyInjection/SamlExtension.php for the exact load() call.

ACS URL exactness. Any mismatch in scheme, host, port, or trailing slash between what you configure and what the IdP stored produces an opaque InvalidResponse/Signature validation failed. Register metadata from your own metadata endpoint and stop hand-typing URLs.

SameSite=Lax silently kills your ACS. The IdP posts cross-site to your ACS endpoint. Modern browsers won't send a Lax session cookie on that POST, so processResponse() sees an empty session and strict-mode replay detection fails. Set the session cookie to SameSite=None; Secure for the ACS route. This is the number one cause of "it worked in staging."

Certificate formatting. x509cert must be a PEM body without the -----BEGIN CERTIFICATE----- markers (php-saml strips/expects the raw base64). Embedding a whole .pem file produces signature failures. privateKey likewise wants the raw base64 lines. When storing in env vars, beware .env mangling newlines — use \n escapes plus a str_replace('\n', "\n", ...) in a compiler pass or settings factory.

Enable strict: true from day one. With strict off, php-saml validates nothing meaningful and you get unsigned-assertion acceptance as a default. Conversely, strict mode also turns on destination/audience/timing checks — which is where clock skew bites. If assertions fail with timing errors, fix NTP rather than setting strict: false.

Replay detection needs session continuity. In strict mode php-saml stores the InResponseTo of the last request. If your login and ACS land on different session domains (load-balanced hosts without sticky sessions, or cookie_domain misconfiguration), you get intermittent "The response was received at ... instead" or missing-request errors. Test through the actual load balancer.

Debugging a bad response. The raw SAMLResponse is a base64+zlib POST body. Decode it:

$xml = gzinflate(base64_decode($_POST['SAMLResponse']));

Then inspect with samltool.com or xmlsec1 --verify. Also set the settings flag debug: true (php-saml's own flag, not Symfony's) and check $auth->getLastErrorReason() alongside getErrors() — the reason string is where the real cause lives. Log the full SAMLResponse only in non-production; it's a bearer credential.

Signed response vs signed assertion. Many IdPs sign only the assertion, others only the response, some both. wantAssertionsSigned: true with an IdP that signs only the response is a very common mismatch. Match your security block to what the IdP actually emits, verified by looking at the XML.

Extension points are thin. There's no event system here — v2.0 added none. Customise by (a) overriding service definitions in your app's services.yaml with a decorated settings provider, or (b) bypassing the bundle service entirely and instantiating OneLogin\Saml2\Auth with your own settings array. Given the package's age and the size of this release, option (b) is often cleaner — the bundle's value is mostly the settings tree and service wiring, and you can reproduce that in ~40 lines while keeping control of dependency updates.

Plan an exit. For anything long-lived, know that the underlying library moved from onelogin/php-saml to SAML-Toolkits/php-saml, and "Support symfony 4" is catching up to a release that is itself now old. Migrating to a maintained Symfony bundle (e.g. hslavich/oneloginsaml-bundle) or a hand-rolled service is a half-day job, not a rewrite. Write your integration behind your own SamlAuthenticator-style interface now so the swap is contained.

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