API-Token-Authentifizierung
API-Token-Authentifizierung
~15 Min. Lesezeit Zuletzt aktualisiert am 8. August 2026
form_login aus Kapitel 27 passt für Browser-Nutzer – für eine spätere API-Erweiterung unseres Aufgaben-Managers (z. B. eine mobile App, GENAU wie in unseren separaten Magento-REST-API-Tutorials) braucht es einen ANDEREN Authentifizierungsweg: zustandslose API-Tokens.
Warum form_login für APIs ungeeignet ist
form_login basiert auf SESSIONS (Kapitel 12) – der Server merkt sich den eingeloggten Zustand serverseitig, der Browser sendet nur ein Session-Cookie mit. Ein API-Client (mobile App, Skript, anderer Server) hat OFT keinen praktikablen Weg, Cookies zu verwalten – ein Bearer-Token im Authorization-Header (GENAU wie wir es in den Magento-REST-API-Tutorials nutzen) ist der Standard-Ansatz für APIs.
Ein apiToken-Feld an User ergänzen
#[ORM\Column(length: 255, nullable: true, unique: true)]
private ?string $apiToken = null;
public function getApiToken(): ?string
{
return $this->apiToken;
}
public function setApiToken(?string $apiToken): static
{
$this->apiToken = $apiToken;
return $this;
}php bin/console make:migration
php bin/console doctrine:migrations:migrateEinen eigenen Authenticator bauen
<?php
declare(strict_types=1);
namespace App\Security;
use App\Repository\UserRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class ApiTokenAuthenticator extends AbstractAuthenticator
{
public function __construct(
private readonly UserRepository $userRepository,
) {
}
public function supports(Request $request): ?bool
{
return $request->headers->has('Authorization')
&& str_starts_with($request->headers->get('Authorization'), 'Bearer ');
}
public function authenticate(Request $request): Passport
{
$authHeader = $request->headers->get('Authorization');
$apiToken = substr($authHeader, 7); // 'Bearer ' abschneiden
if ($apiToken === '') {
throw new CustomUserMessageAuthenticationException('Kein API-Token angegeben.');
}
return new SelfValidatingPassport(
new UserBadge($apiToken, function (string $apiToken) {
$user = $this->userRepository->findOneBy(['apiToken' => $apiToken]);
if ($user === null) {
throw new CustomUserMessageAuthenticationException('Ungültiger API-Token.');
}
return $user;
})
);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): JsonResponse
{
return new JsonResponse(['fehler' => $exception->getMessage()], 401);
}
}supports() prüft, ob DIESER Authenticator überhaupt zuständig ist (Bearer-Token vorhanden) – authenticate() lädt den passenden Nutzer. SelfValidatingPassport (statt Passport mit separatem Passwort-Badge wie bei form_login) signalisiert: der Token IST bereits der vollständige Beweis der Identität, KEIN zusätzlicher Passwort-Check nötig.
Eine separate Firewall für API-Routen
security:
firewalls:
api:
pattern: ^/api
stateless: true
custom_authenticators:
- App\Security\ApiTokenAuthenticator
main:
# ... wie in Kapitel 27, für den Browser-Bereich ...
access_control:
- { path: ^/api, roles: ROLE_USER }
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/projects, roles: ROLE_USER }stateless: true ist ENTSCHEIDEND: verbietet Symfony, für diese Firewall eine Session anzulegen – JEDE Anfrage muss ihren Token ERNEUT mitschicken, GENAU wie eine echte API funktionieren soll. pattern: ^/api sorgt dafür, dass NUR Routen unter /api/... diesen Authenticator nutzen, während /projects weiter über form_login (Firewall main) läuft.
Achtung: MEHRERE Firewalls werden von OBEN nach UNTEN geprüft, die ERSTE, deren pattern passt, gewinnt – die spezifischere api-Firewall MUSS deshalb VOR der allgemeineren main-Firewall stehen, sonst würde /api/... fälschlich von main behandelt.
Den API-Token testen
TOKEN="abc123..."
curl -H "Authorization: Bearer $TOKEN" https://aufgaben-manager.local/api/projectsTipp: Der genau gleiche Aufbau (Bearer-Token im Authorization-Header) wie in unseren separaten React/React-Native-&-Magento-Tutorials – dasselbe Prinzip taucht in FAST jeder modernen API wieder auf, unabhängig vom verwendeten Backend-Framework.