halloverden/symfony-authenticator-restrictor-bundle
A Symfony bundle for restricting which authenticators are available in your application. Configure rules to control when specific authentication methods can be used, giving fine-grained control over login flows without custom security code.
This is a Symfony bundle (not a Laravel package) — there is no guard/middleware analogue here, so if you're in a Laravel codebase the equivalent is per-guard route groups with auth:<guard> middleware, and this bundle offers nothing. For a Symfony service running multiple authenticators inside a single firewall, this bundle lets you scope which authenticator runs on which route.
1. Verify the source before you install. At 0 stars, 0 dependents, and no visible release cadence, this is a "read all ~100–200 lines" package, not a "trust the abstraction" package. Look at, in order:
src/ for an Attribute/ folder — determines whether restriction is attribute-driven (on controllers) or config-driven.DependencyInjection/ and any CompilerPass — this is where authenticators get decorated and where the matcher semantics live.2. Install and register.
composer require halloverden/symfony-authenticator-restrictor-bundle
Bundles generally do not auto-register on a plain Symfony install:
// config/bundles.php
return [
// ...
Halloverden\SymfonyAuthenticatorRestrictorBundle\SymfonyAuthenticatorRestrictorBundle::class => ['all' => true],
];
3. Clear the cache — this bundle works at container-compile time, so bin/console cache:clear is part of the install, not an afterthought.
4. First use case to reach for: a firewall that hosts both a form login and an API token authenticator, where POST /api/* should only ever hit the token authenticator and /login should only ever hit the form. Before this bundle the usual answer is splitting the firewall by pattern, which duplicates context, remember_me, logout, and access-control config and breaks shared sessions. Keep one firewall, scope the authenticators by route.
Start with a spike branch and an integration test asserting both the allowed route and the denied route. The value proposition is a negative control — an authenticator that must not fire — so a smoke test that only proves the happy path proves nothing.
Configure allowed authenticators per firewall / request context. The core usage is a mapping from authenticator service IDs to route or path patterns. Confirm the exact key names against the source rather than guessing — a wrong service ID is the most common silent failure mode (see Gotchas).
Incremental auth modernization (the strongest pattern). Adding passkeys, OAuth2/OIDC, or an API-token authenticator alongside a legacy form login is normally a big-bang cutover. Instead, land the new mechanism scoped to /app/* while /legacy/* keeps the old one. Same firewall, same context — sessions stay shared across the migration window. This shortens the migration and is a real de-risking lever for a roadmap item.
Isolate JSON login from token auth. JSON-login authenticators that short-circuit on POST and token authenticators that throw on non-API paths produce bugs that look like routing problems but are auth-ordering problems. Multiple authenticators in one firewall mean every authenticator sees every request. Scoping them by route removes the interference with a narrow blast radius.
Treat it as a decorator, and prototype the decorator first. The capability is small: a request matcher plus an AuthenticatorInterface decorator and a compiler pass that wires it into the service definitions. If you need more than about a day of engineering to reproduce it, use the package; if you can write it in ~50 lines, prefer first-party code — it removes a dependency from your authentication path and you own the test suite. Either way, put it behind a feature flag until one full release cycle has passed.
Test matrix, not smoke tests. Enumerate every authenticator × route pair and assert the allow and the deny:
POST /api/thing with the token authenticator allowed, form authenticator denied.GET /login with form allowed, token denied.// Sketch — the intent matters more than the exact helper names
$this->client->request('POST', '/api/thing', server: ['CONTENT_TYPE' => 'application/json']);
$this->assertResponseIsSuccessful();
// Negative control: the form authenticator must NOT process this
$this->client->request('POST', '/login');
$this->assertResponseStatusCodeSame(401);
Keep it off the hot path when you can. Runtime cost is a request matcher evaluation per request — negligible — but do not stack it with other request-time decorators that also short-circuit the authenticator pipeline. Compiler-pass ordering is the place where two such bundles collide.
Verify the unmatched-route default before anything else. If the bundle is wrong in either direction when no pattern matches, you get an auth bypass (weaker authenticator silently active) or a lockout (the only authenticator suppressed, 401s in production for a subset of traffic). Read the matcher and write the test that proves it.
Route-name matching is fragile against renames. Path matching is fragile against prefix/locale changes. Whichever you choose, note it in the runbook and add a test that fails if the route is renamed or the API prefix changes. This is the most likely production break.
A config typo is a silent no-op. Wrong authenticator service ID or wrong route name and the bundle simply does nothing — which looks like success until an audit. After wiring config, verify the compiler pass actually rewrote the service definition (dump the container or add a temporary assertion) rather than assuming.
Check lazy firewalls, stateless firewalls, and multi-authenticator firewalls explicitly. Lazy firewalls and the authenticator deprecations in Symfony 6.x/7.x are where auth internals churn. Confirm the declared symfony/security-bundle and PHP constraints against your platform — run composer why-not in CI before merging.
Test sub-requests, ESI fragments, and forwarded requests behind a proxy. Matcher semantics can differ there, and the failure will not show up in a happy-path test.
Upgrades will break loudly or badly. Compiler-pass ordering conflicts surface at container compile time (good) or at runtime (bad). Budget a day or two per Symfony major upgrade. Most likely real-world cost isn't the upgrade itself — it's upstream going quiet and its framework constraint capping your Symfony version, blocking the upgrade.
Plan to fork. Zero stars, zero dependents, no advisories channel, and no maintainer responsiveness guarantee means a vulnerability scanner will never emit an advisory for it — the fork is the security control. Pin the exact version in composer.lock, add it to the dependency allowlist, and record an owner and a fork plan in the risk log.
Write the onboarding note that actually matters. Not docs for the bundle — an internal note recording the matcher semantics, the unmatched-route default, and the path to the test that proves it. That's what the next person needs at 2 a.m., and it is the deliverable regardless of whether you adopt the package or reimplement the pattern.
When to skip it entirely: a firewall pattern, a custom RequestMatcher, entry_point, security.access_control, or per-route IS_AUTHENTICATED_* rules already express the constraint — or expressing it that way doesn't force a firewall split. Native config wins every time; it's supported, documented, and survives upgrades.
How can I help you explore Laravel packages today?