gauss-allianz/shibboleth-authentication-bundle
Symfony bundle that adds Shibboleth single sign-on authentication to your application, letting you integrate federated identity providers and protect routes with Shibboleth-based login.
First, the framework mismatch. gauss-allianz/shibboleth-authentication-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no config/shibboleth.php, and no Laravel guard. As a Laravel developer you have two realistic paths:
Auth::viaRequest guard (see below).Security/ directory and adapt the authenticator logic; the hard part (Shibboleth SP integration at the web-server level) is framework-agnostic anyway.The core idea is the same in either case: Apache/nginx with mod_shib (the Shibboleth Service Provider) authenticates the user, then injects attributes into the request environment — Shib-Identity-Provider, eppn, mail, persistent-id, Shib-Session-Id, etc. Your application never talks SAML itself; it just reads pre-authenticated server variables. The bundle exists to wire those variables into Symfony's security firewall.
Minimal steps (Symfony path):
composer require gauss-allianz/shibboleth-authentication-bundle
// config/bundles.php
GaussAllianz\ShibbolethAuthenticationBundle\ShibbolethAuthenticationBundle::class => ['all' => true],
# config/packages/security.yaml
security:
firewalls:
shibboleth:
pattern: ^/protected
stateless: true
shibboleth: ~ # verify exact key in the bundle's Configuration.php
Where to look first:
DependencyInjection/Configuration.php — the authoritative list of config keys. READMEs on low-traffic bundles drift; this file does not.Security/ — the authenticator/listener and entry point that reads $_SERVER.mod_shib not running in lazy mode or headers not reaching PHP-FPM.First use case: protect one route (/protected), have the bundle map eppn to a user, log them in, and let logout redirect to /Shibboleth.sso/Logout. Prove the whole loop end-to-end against a real IdP (or a mod_shib test IdP) before touching user provisioning.
Laravel equivalent for the first use case:
// AuthServiceProvider
Auth::viaRequest('shibboleth', function ($request) {
$eppn = $request->server('eppn') ?? $request->server('persistent-id');
return $eppn ? User::firstOrCreate(['shib_id' => $eppn]) : null;
});
Dedicated, stateless firewall with an entry point. Shibboleth users have no password and no form. The firewall should be stateless: true and its entry point should redirect to the SP login handler with a target back to the original URL, preserving deep links:
/Shibboleth.sso/Login?entityID=<idp>&target=https://app.example.com/protected/resource
Just-in-time provisioning via a custom user provider. The dominant pattern is a UserProviderInterface that looks up by persistent-id (the only stable identifier), falls back to eppn, and creates the local user on first sight. Every subsequent login should refresh mutable attributes (mail, displayName, affiliation) rather than only setting them at creation:
public function loadUserByIdentifier(string $id): UserInterface
{
$user = $this->repo->findOneBy(['shibPersistentId' => $id])
?? new User();
$user->setShibPersistentId($id)
->setEmail($_SERVER['mail'] ?? $user->getEmail())
->setDisplayName($_SERVER['displayName'] ?? $user->getDisplayName());
return $user;
}
Separate the "who is this" decision from the "what attributes arrived" data. Parse $_SERVER into a small ShibbolethAttributes value object once, in the authenticator, so the provider and any later listeners operate on typed data instead of raw superglobals. This makes testing trivial and isolates the one place that touches $_SERVER.
Logout has two halves. Local logout ($session->invalidate() / Auth::logout()) does not end the IdP session — the user will be silently re-authenticated on the next request. Pair it with a redirect to the SP logout endpoint (/Shibboleth.sso/Logout), and enable Single Logout at the IdP if you need the IdP session killed too. Pattern:
// LogoutSuccessListener
return new RedirectResponse('/Shibboleth.sso/Logout?return=' . urlencode($this->homeUrl));
Guard public routes strictly. Never let the Shibboleth firewall cover /, /health, /assets. Restrict by pattern: to the paths that genuinely require federated identity; otherwise anonymous traffic will hit the entry point redirect loop.
Multi-IdP / discovery. If multiple institutions are supported, either use Shibboleth's Discovery Service (/Shibboleth.sso/Login?target=... with DS enabled) or pass entityID explicitly from an institution-selection page. Store the chosen Shib-Identity-Provider on the user so you can audit which federation they came from.
Testing without an IdP. In feature tests, set the attributes before dispatching the request and assert the login happens:
$_SERVER['eppn'] = '[email protected]';
$_SERVER['persistent-id'] = 'https://idp.example.edu!https://app.example.com!abc123';
$_SERVER['Shib-Identity-Provider'] = 'https://idp.example.edu/idp/shibboleth';
$this->client->request('GET', '/protected');
$this->assertResponseIsSuccessful();
Header spoofing is the number-one security risk. Shibboleth attributes arrive as HTTP headers (Shib-* or similar) populated by Apache. If any other path to your app exists — a direct PHP-FPM port, a CDN origin bypass, a staging host without mod_shib — an attacker can send Shib-eppn: [email protected] and impersonate anyone. Mitigations: bind the app to localhost, strip all Shib-*/eppn/mail headers at the edge proxy, and only trust attributes set by the SP. Treat "the bundle trusts $_SERVER" as an architectural assumption, not a bug.
eppn is not a stable identifier. It changes on affiliation changes and is reassigned between people at many institutions. Key your users table on persistent-id (or Subject-ID/pairwise-id in newer deployments). Using eppn as a primary key is a well-known way to hand one user another user's account.
Symfony version compatibility is the biggest practical blocker. This bundle has 0 stars, 0 dependents, and likely predates Symfony 5's security rewrite (Guard → AuthenticatorInterface, UserProviderInterface::loadUserByUsername → loadUserByIdentifier). Check composer.json constraints before committing; expect to patch the authenticator if you're on Symfony 5.3+/6.x. On Laravel there is nothing to check — you're writing it yourself.
Missing attributes are normal. IdPs only release what the federation's attribute release policy permits. Never hard-depend on mail, displayName, or eduPersonAffiliation — always have a fallback (synthesized display name from eppn, null email) and log when an expected attribute is absent. "It works for our test IdP but not the partner's" is almost always attribute release, not code.
mod_shib lazy sessions vs. active sessions. In lazy mode the SP doesn't redirect until the app requests /Shibboleth.sso/Login; in active mode it redirects on every request to a protected location. If your entry-point redirect seems to fire twice or not at all, check ShibRequestSetting requireSession and the AuthType shibboleth / ShibRequestSetting directives in the vhost. This is configured outside PHP and is the most common source of confusing behavior.
Debugging checklist when nothing works:
phpinfo() or a raw var_dump($_SERVER) on a protected path — do Shib-* keys exist at all?Shib-Session-Id present? If not, the SP isn't authenticating you.Shib-Identity-Provider correct? If not, you're hitting the wrong IdP.$_SERVER have the attribute but the bundle doesn't see it? Check variables_order in php.ini and PHP-FPM's clear_env — environment variables can be dropped between Apache and FPM.Config quirks. Bundle config keys for low-traffic packages are under-documented; the DI Configuration.php is the source of truth. Also note that the same attribute may be exposed under different names depending on mod_shib config (eppn vs eduPersonPrincipalName vs Shib-InetOrgPerson-epPN) — normalize in one place.
Extension points. Favor decorating/replacing the user provider over forking the authenticator: the provider is the natural seam for provisioning rules, duplicate-account merging, role mapping from eduPersonEntitlement/isMemberOf, and audit logging. If you do need a fork, keep the diff limited to the Security/ namespace so upstream merges stay possible — though with zero dependents, assume upstream is dead and treat your fork as the maintained version.
Strategic advice. With 0 stars and 0 dependents, there's no community to lean on for security patches in an authentication component. If you're on Symfony, evaluate whether a maintained SAML bundle or a thin in-house authenticator (the code above is genuinely small) is a better bet than adopting an unmaintained dependency in your auth path. If you're on Laravel, skip the package entirely — implement the viaRequest guard, put the header-stripping rule in your proxy config, and you'll have a smaller, fully-understood surface than any port would give you.
How can I help you explore Laravel packages today?