knpuniversity/guard
KnpUniversity Guard adds simple, flexible guard-based authentication to Symfony. Define a guard class to handle credentials, user loading, success or failure, and token creation, making custom login flows and API authentication easier.
Read this first: knpuniversity/guard is a Symfony bundle, not a Laravel package. It backports Symfony's Guard authentication system (native from Symfony 2.8) to Symfony 2.6/2.7. There is no service provider, no facade, no Auth::extend integration, and no path to composer require it into a Laravel 9/10/11 app. For a Laravel developer, day-to-day leverage from this repository is conceptual reference, not code reuse.
Where to look first
README.md — shows the single-class GuardAuthenticatorInterface contract. Skim it in 10 minutes; that flow is the design you'll mirror.KnpUGuardBundle / DI extension — only if you're actually maintaining a Symfony 2.6/2.7 app. Modern readers can skip it.AuthenticatorInterface (5.3+) — this is where the package's ideas actually live and get maintained.Your first use case (Laravel)
Extract the flow model, then implement it with first-party Laravel tooling. The Guard lifecycle is:
getCredentials → getUser → checkCredentials
→ createAuthenticatedToken (onAuthenticationSuccess)
→ onAuthenticationFailure / start (challenge)
Laravel counterpart:
// config/auth.php
'guards' => [
'api-key' => ['driver' => 'api-key', 'provider' => 'users'],
],
// app/Providers/AuthServiceProvider.php
Auth::extend('api-key', function ($app, $name, array $config) {
return new ApiKeyGuard(
Auth::createUserProvider($config['provider']),
$app['request'],
);
});
class ApiKeyGuard implements Guard
{
use GuardHelpers;
public function __construct(protected UserProvider $provider, protected Request $request) {}
public function user(): ?Authenticatable
{
if ($this->user !== null) return $this->user;
$key = $this->request->header('X-Api-Key');
if (!$key) return null;
$user = $this->provider->retrieveByCredentials(['api_key' => $key]);
return $this->user = $user; // checkCredentials equivalent
}
public function validate(array $credentials = []): bool { /* ... */ }
public function check(): bool { return $this->user() !== null; }
}
If you're on a Laravel version where Fortify/Sanctum already covers the flow (session login, API tokens, 2FA, remember-me), use those instead of writing a custom guard — the same advice this package's own assessment gives: use the framework.
Only-if-you-must (legacy Symfony 2.6/2.7)
composer require knpuniversity/guard
// app/AppKernel.php
$bundles[] = new KnpU\GuardBundle\KnpUGuardBundle();
# app/config/security.yml
security:
providers:
users: { entity: { class: AppBundle\Entity\User, property: username } }
firewalls:
main:
anonymous: ~
guard:
authenticators:
- app.form_authenticator
- app.api_token_authenticator
Then implement one authenticator class per flow. Treat this as a stopgap with a scheduled exit, not a foundation.
Pattern 1 — One authenticator per flow, not per endpoint. The whole point of Guard is that a single class owns credentials, user loading, credential checking, success, failure, and the challenge response. In Laravel that maps to one custom Guard (or one Authenticator) per flow. Keep them small and flow-specific: FormLoginGuard, ApiTokenGuard, SocialLoginGuard.
class ApiTokenGuard implements Guard
{
public function user(): ?Authenticatable
{
if ($this->user) return $this->user;
$token = $this->request->bearerToken();
if (!$token) return null;
return $this->user = $this->provider->retrieveByCredentials(['api_token' => $token]);
}
}
Pattern 2 — Multiple authenticators on one firewall (Symfony). Guard's real value is letting several authenticators coexist (form + remember-me + token) with a first-class challenge flow:
firewalls:
main:
guard:
entry_point: app.form_authenticator
authenticators:
- app.api_token_authenticator
- app.form_authenticator
Laravel equivalent: multiple guards on request via middleware, or one guard that inspects the request shape.
// app/Http/Middleware/ChooseAuthGuard.php
public function handle($request, Closure $next)
{
config(['auth.defaults.guard' => $request->expectsJson() ? 'api' : 'web']);
return $next($request);
}
Pattern 3 — Stateless vs stateful split. Guard's start() is what renders the challenge (redirect to /login or 401). Mirror that with Laravel middleware branching on $request->expectsJson() or Accept headers:
class Authenticate
{
protected function redirectTo($request)
{
return $request->expectsJson() ? null : route('login');
}
}
Pattern 4 — Remember-me. Guard supports remember-me tokens in the authenticator. In Laravel this is Auth::attempt($creds, $remember = true) plus a remember_token column — the built-in SessionGuard handles it. Don't roll it yourself.
Pattern 5 — Social login. Guard-style social authenticators (getCredentials reads the provider code, getUser exchanges it, checkCredentials is usually return true) map cleanly to Laravel Socialite:
Route::get('/auth/{provider}/callback', function ($provider) {
$social = Socialite::driver($provider)->user();
$user = User::firstOrCreate(['email' => $social->getEmail()], [
'name' => $social->getName(),
'provider_id' => $social->getId(),
]);
Auth::login($user, remember: true);
return redirect('/dashboard');
});
Pattern 6 — Migration mapping (legacy Symfony → modern). If your job is removing this dependency:
| Guard method (this package) | Native Symfony Guard (2.8+) | Modern AuthenticatorInterface (5.3+) |
|---|---|---|
getCredentials(Request) |
same | getCredentials(Request) |
getUser($credentials, UserProviderInterface) |
same | getUser(Passport) |
checkCredentials($user, $credentials) |
same | Passport validation |
createAuthenticatedToken |
same | createToken(Passport) |
onAuthenticationSuccess |
same | onAuthenticationSuccess |
onAuthenticationFailure |
same | onAuthenticationFailure |
start(Request) |
start(Request) |
start(Request) |
supportsRememberMe() |
same | via RememberMeBadge |
The work is mechanical: one file per authenticator, re-point method signatures, replace the DI tags.
Pattern 7 — CI guard against reintroduction. If this package shows up in a scan, treat it as a legacy marker. Add a lint rule:
# scripts/check-forbidden-deps.sh
if grep -q 'knpuniversity/guard' composer.json; then
echo "Forbidden dependency: knpuniversity/guard (use native auth)" >&2
exit 1
fi
composer require fails on anything current; --ignore-platform-reqs gets you a file that cannot bootstrap because there is no Symfony kernel, container, or event dispatcher. If a teammate proposes it for a Laravel app, stop the conversation there.Guard. Illuminate\Contracts\Auth\Guard, Auth::guard(), and $request->guard() have nothing to do with this Symfony Guard. Keep the vocabulary separate in reviews and tickets; confusion here wastes real time.checkCredentials code verbatim. The 2015 idioms use hash_equals patterns and sometimes plain == comparisons. Copying them into a modern codebase imports a timing-safe-comparison bug class with no tests to catch it. In Laravel, use Hash::check($plain, $user->password) — and let SessionGuard do it.getUser() is where user enumeration defects hide. Guard distinguishes "user not found" from "bad password" through onAuthenticationFailure. In your Laravel rewrite, return the same generic error message and log the specific reason:
if (! $user || ! Hash::check($password, $user->password)) {
Log::warning('auth.failed', ['email' => $email, 'ip' => $request->ip()]);
return back()->withErrors(['email' => __('auth.failed')]);
}
start() is the challenge, not the failure. It runs before any credentials are checked — the "you must authenticate first" response, not "your password was wrong." In Laravel that's middleware('auth') redirecting to route('login'); the "wrong password" case is a separate flash-and-back. Getting these two conflated produces infinite-redirect bugs in both frameworks.remember_me: is a separate firewall key alongside guard:. Forgetting it and expecting supportsRememberMe() to work is a common trap. In Laravel, Auth::attempt($creds, true) plus a remember_token column does the whole job — the SessionGuard handles it.getCredentials() wins. Put token authenticators before form authenticators or every JSON request will be redirected to login. The Laravel analogue is guard middleware order — same class of bug.provider is required on every guard block. A guard without a provider fails at request time with a confusing container error, not at boot. This bites in both Symfony (guard.provider) and Laravel ('api-key' => ['driver' => ..., 'provider' => 'users']).onAuthenticationFailure and start(). Write tests for: bad password, unknown user, already-authenticated user re-authenticating, and JSON vs HTML request. These are the tests that survive a migration.GuardAuthenticatorInterface methods are almost 1:1 with the modern AuthenticatorInterface. If you must maintain a legacy app on this, keep each authenticator a thin adapter so a later upgrade is a rename, not a rewrite.composer.json, do not vendor it, do not open an advisory ticket expecting a maintainer — there isn't one.How can I help you explore Laravel packages today?