Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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:migrate

Einen eigenen Authenticator bauen

src/Security/ApiTokenAuthenticator.php
<?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

config/packages/security.yaml
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/projects

Tipp: 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.