„Unser API-Token wurde kompromittiert – und wir wissen nicht mal, wer Zugriff hatte.“ – Solche Anrufe kennen wir aus über 15 Jahren Consulting-Erfahrung leider zu gut. OAuth2 ist der Industriestandard für sichere API-Authentifizierung, doch die Implementierung in Symfony-Projekten wirft bei vielen Teams Fragen auf. Mit dem league/oauth2-server-bundle steht euch ein ausgereiftes, offiziell vom Symfony-Core-Team unterstütztes Werkzeug zur Verfügung, das wir euch heute im Detail vorstellen.
Bei Never Code Alone unterstützen wir Teams seit über 15 Jahren bei der Implementierung sicherer Authentifizierungslösungen. Softwarequalität, Open Source und Remote Consulting sind unsere Kernkompetenzen – und genau diese Expertise fließt in diesen Praxisleitfaden ein.
Was ist OAuth2 und worin unterscheidet es sich von JWT und OpenID Connect?
Diese Frage hören wir in fast jedem Projekt, und sie ist berechtigt. OAuth2 ist ein Autorisierungs-Framework, das regelt, wie Anwendungen Zugriff auf Ressourcen erhalten – ohne Passwörter direkt weiterzugeben. JWT (JSON Web Token) hingegen ist ein Token-Format, das häufig innerhalb von OAuth2 verwendet wird. OpenID Connect wiederum ist eine Authentifizierungsschicht auf OAuth2, die zusätzlich die Identität des Nutzers verifiziert.
In der Praxis bedeutet das: OAuth2 definiert den Ablauf, JWT ist das Transportmedium, und OpenID Connect fügt Nutzerinformationen hinzu. Für reine API-Authentifizierung reicht OAuth2 mit JWT-Tokens. Benötigt ihr Single Sign-On oder Nutzerprofile, kommt OpenID Connect ins Spiel.
// OAuth2 liefert einen Access Token
{
"access_token": "eyJ0eXAiOiJKV1QiLC...",
"token_type": "Bearer",
"expires_in": 3600
}
// OpenID Connect ergänzt einen ID Token mit Nutzer-Claims
{
"access_token": "eyJ0eXAiOiJKV1QiLC...",
"id_token": "eyJ0eXAiOiJKV1QiLC...",
"token_type": "Bearer"
}
Installation und Grundkonfiguration mit Symfony 7
Das league/oauth2-server-bundle ist seit Ende 2025 in Version 1.1.0 verfügbar und unterstützt Symfony 6.4, 7.0 und 8.0. Es ersetzt das bisherige trikoder/oauth2-bundle und wurde in Abstimmung mit dem Symfony-Core-Team entwickelt.
composer require league/oauth2-server-bundle
Falls ihr Symfony Flex nutzt, wird das Bundle automatisch konfiguriert. Andernfalls registriert ihr es manuell:
// config/bundles.php
return [
// ...
LeagueBundleOAuth2ServerBundleLeagueOAuth2ServerBundle::class => ['all' => true],
];
Wie generiere ich die kryptografischen Schlüssel korrekt?
Der OAuth2-Server benötigt ein RSA-Schlüsselpaar zum Signieren der JWT-Tokens sowie einen Encryption Key. Die Schlüsselgenerierung ist ein kritischer Schritt, den ihr nicht überspringen solltet.
# Verzeichnis für Schlüssel erstellen
mkdir -p var/keys
# Private Key generieren (ohne Passphrase für Entwicklung)
openssl genrsa -out var/keys/private.key 2048
# Public Key ableiten
openssl rsa -in var/keys/private.key -pubout -out var/keys/public.key
# Encryption Key generieren
php -r "echo base64_encode(random_bytes(32));"
Die Konfiguration erfolgt in config/packages/league_oauth2_server.yaml:
league_oauth2_server:
authorization_server:
private_key: '%kernel.project_dir%/var/keys/private.key'
private_key_passphrase: null
encryption_key: '%env(OAUTH2_ENCRYPTION_KEY)%'
encryption_key_type: plain
resource_server:
public_key: '%kernel.project_dir%/var/keys/public.key'
Speichert den Encryption Key niemals im Repository. Nutzt Environment-Variablen und euren Secret Manager.
Welche Grant Types gibt es und wann nutze ich welchen?
Das Bundle unterstützt alle Standard-Grant-Types. Die Wahl hängt von eurem Use Case ab:
Authorization Code Grant – Der sicherste Flow für Web-Anwendungen mit Backend. Der Nutzer autorisiert die Anwendung, erhält einen Authorization Code, der dann gegen einen Access Token getauscht wird.
Client Credentials Grant – Für Machine-to-Machine-Kommunikation ohne Nutzerinteraktion. Ideal für Microservices oder Batch-Jobs.
Refresh Token Grant – Ermöglicht das Erneuern abgelaufener Access Tokens ohne erneute Nutzeranmeldung.
Password Grant – Veraltet und nicht empfohlen. Nur für Legacy-Migrationen nutzen.
Implicit Grant – Ebenfalls veraltet. Nutzt stattdessen Authorization Code mit PKCE.
# config/packages/league_oauth2_server.yaml
league_oauth2_server:
authorization_server:
grant_types:
authorization_code:
enable: true
auth_code_ttl: PT10M
access_token_ttl: PT1H
refresh_token_ttl: P1M
require_code_challenge_for_public_clients: true
client_credentials:
enable: true
access_token_ttl: PT1H
refresh_token:
enable: true
access_token_ttl: PT1H
refresh_token_ttl: P1M
password:
enable: false
implicit:
enable: false
Was ist PKCE und wie implementiere ich es für Public Clients?
PKCE (Proof Key for Code Exchange) schützt den Authorization Code Flow gegen Interception-Angriffe. Für Public Clients wie Single Page Applications oder Mobile Apps ist PKCE seit 2025 praktisch Pflicht.
Das Bundle unterstützt PKCE out-of-the-box. Aktiviert ihr require_code_challenge_for_public_clients, verweigert der Server Requests ohne Code Challenge:
grant_types:
authorization_code:
require_code_challenge_for_public_clients: true
Der Client generiert einen zufälligen Code Verifier und berechnet daraus die Code Challenge:
// Client-Seite: Code Verifier generieren
$codeVerifier = bin2hex(random_bytes(32));
// Code Challenge berechnen (SHA-256)
$codeChallenge = rtrim(strtr(
base64_encode(hash('sha256', $codeVerifier, true)),
'+/', '-_'
), '=');
// Authorization Request mit Challenge
$authUrl = sprintf(
'%s/authorize?response_type=code&client_id=%s&redirect_uri=%s&code_challenge=%s&code_challenge_method=S256',
$serverUrl,
$clientId,
urlencode($redirectUri),
$codeChallenge
);
Beim Token-Request sendet der Client den ursprünglichen Code Verifier, den der Server verifiziert.
Wie konfiguriere ich Token-Lebensdauern sinnvoll?
Die richtige Balance zwischen Sicherheit und Nutzererfahrung ist entscheidend. Kurze Access Token TTLs minimieren das Risiko bei Kompromittierung, erzeugen aber mehr Token-Refresh-Anfragen.
Unsere Empfehlungen aus der Praxis:
| Token-Typ | Empfohlene TTL | Begründung |
|---|---|---|
| Access Token | 15-60 Minuten | Kurz genug für Schadensbegrenzung |
| Refresh Token | 7-30 Tage | Balance zwischen Sicherheit und UX |
| Authorization Code | 5-10 Minuten | Nur für initialen Exchange |
access_token_ttl: PT15M # 15 Minuten
refresh_token_ttl: P7D # 7 Tage
auth_code_ttl: PT5M # 5 Minuten
Bei hochsensiblen APIs reduziert ihr die Werte weiter. Für interne Microservices mit Client Credentials können längere TTLs akzeptabel sein.
Wie richte ich Scopes für feingranulare Berechtigungen ein?
Scopes definieren, auf welche Ressourcen ein Token zugreifen darf. Das Bundle vergibt automatisch Rollen im Format ROLE_OAUTH2_<SCOPE>, die ihr in euren Security-Konfigurationen nutzt.
# config/packages/league_oauth2_server.yaml
league_oauth2_server:
scopes:
available:
- read
- write
- profile
- admin
default:
- read
role_prefix: ROLE_OAUTH2_
In euren Controllern prüft ihr die Scopes über das Standard-Symfony-Security-System:
#[Route('/api/users', name: 'api_users_list')]
#[IsGranted('ROLE_OAUTH2_READ')]
public function listUsers(): JsonResponse
{
// Nur mit 'read' Scope erreichbar
}
#[Route('/api/users', methods: ['POST'])]
#[IsGranted('ROLE_OAUTH2_WRITE')]
public function createUser(): JsonResponse
{
// Erfordert 'write' Scope
}
Wie handle ich Refresh Tokens bei instabilen Verbindungen?
Mobile Apps und PWAs kämpfen oft mit Netzwerkunterbrechungen während des Token-Refresh. Ein klassisches Problem: Der Server hat den alten Refresh Token invalidiert, aber der Client hat den neuen nie erhalten.
Das Bundle bietet die Option, bei jedem Refresh einen neuen Refresh Token auszugeben:
refresh_token:
enable: true
access_token_ttl: PT1H
refresh_token_ttl: P1M
Für robuste Clients empfehlen wir folgende Strategien:
- Retry mit Backoff: Bei Netzwerkfehlern mehrfach versuchen
- Token-Speicherung: Alten und neuen Token bis zur Bestätigung aufbewahren
- Grace Period: Alte Tokens kurzzeitig parallel akzeptieren
// Client-seitige Retry-Logik
public function refreshToken(string $refreshToken): TokenResponse
{
$maxRetries = 3;
$delay = 1000; // ms
for ($i = 0; $i < $maxRetries; $i++) {
try {
return $this->httpClient->post('/token', [
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => $this->clientId,
]);
} catch (NetworkException $e) {
if ($i === $maxRetries - 1) throw $e;
usleep($delay * 1000 * pow(2, $i));
}
}
}
Wie integriere ich OpenID Connect in meine Symfony-API?
Seit Symfony 6.3 unterstützt das Framework OpenID Connect nativ. Ihr könnt OIDC-Token sowohl validieren als auch eigene ID Tokens ausstellen.
Für die Token-Validierung gegen einen externen OIDC-Provider:
# config/packages/security.yaml
security:
firewalls:
api:
pattern: ^/api
stateless: true
access_token:
token_handler:
oidc_user_info: 'https://auth.example.com/userinfo'
Alternativ validiert ihr JWT-basierte ID Tokens lokal:
security:
firewalls:
api:
access_token:
token_handler:
oidc:
algorithms: ['RS256']
keyset: '%env(OIDC_JWKS)%'
audience: 'my-api'
issuers: ['https://auth.example.com']
Für die Ausgabe eigener ID Tokens erweitert ihr das AccessToken-Entity des Bundles:
namespace AppEntity;
use LeagueBundleOAuth2ServerBundleEntityAccessToken as BaseAccessToken;
class AccessToken extends BaseAccessToken
{
public function convertToJWT(): Token
{
$token = parent::convertToJWT();
// OpenID Connect Claims hinzufügen
$user = $this->getUserIdentifier();
$token = $token->withClaim('sub', $user->getId())
->withClaim('email', $user->getEmail())
->withClaim('name', $user->getFullName());
return $token;
}
}
Wie teste ich meinen OAuth2-Server?
Ein OAuth2-Server ohne Tests ist eine Zeitbombe. Das Bundle bringt Testing-Utilities mit, die ihr nutzen solltet.
Für Unit-Tests der Token-Generierung:
namespace AppTestsOAuth;
use LeagueBundleOAuth2ServerBundleManagerClientManagerInterface;
use SymfonyBundleFrameworkBundleTestKernelTestCase;
class TokenGenerationTest extends KernelTestCase
{
public function testClientCredentialsGrant(): void
{
self::bootKernel();
$client = $this->createMock(ClientInterface::class);
$client->method('getIdentifier')->willReturn('test-client');
$client->method('isConfidential')->willReturn(true);
$response = $this->client->request('POST', '/token', [
'body' => [
'grant_type' => 'client_credentials',
'client_id' => 'test-client',
'client_secret' => 'test-secret',
],
]);
$this->assertResponseIsSuccessful();
$this->assertJsonContains(['token_type' => 'Bearer']);
}
}
Für End-to-End-Tests mit echtem OAuth-Flow:
public function testAuthorizationCodeFlow(): void
{
// 1. Authorization Request
$this->client->request('GET', '/authorize', [
'query' => [
'response_type' => 'code',
'client_id' => 'test-client',
'redirect_uri' => 'https://client.example.com/callback',
'scope' => 'read',
'state' => 'xyz',
],
]);
// 2. User Login simulieren
$this->client->loginUser($this->testUser);
// 3. Consent erteilen und Code erhalten
$this->client->submitForm('authorize', ['consent' => true]);
$redirectUrl = $this->client->getResponse()->headers->get('Location');
parse_str(parse_url($redirectUrl, PHP_URL_QUERY), $params);
$this->assertArrayHasKey('code', $params);
// 4. Code gegen Token tauschen
$this->client->request('POST', '/token', [
'body' => [
'grant_type' => 'authorization_code',
'code' => $params['code'],
'redirect_uri' => 'https://client.example.com/callback',
'client_id' => 'test-client',
'client_secret' => 'test-secret',
],
]);
$this->assertResponseIsSuccessful();
}
Nutzt Postman oder OAuth-Playgrounds für manuelle Tests während der Entwicklung. Die Swagger/OpenAPI-Dokumentation hilft beim Verstehen der Endpoints.
Client-Verwaltung über die Kommandozeile
Das Bundle stellt CLI-Commands für die Client-Verwaltung bereit:
# Neuen Client erstellen
php bin/console league:oauth2-server:create-client my-app
--scope=read --scope=write
--grant-type=authorization_code
--grant-type=refresh_token
--redirect-uri=https://myapp.example.com/callback
# Client aktualisieren
php bin/console league:oauth2-server:update-client my-app
--add-scope=admin
# Client löschen
php bin/console league:oauth2-server:delete-client my-app
# Abgelaufene Tokens aufräumen
php bin/console league:oauth2-server:clear-expired-tokens
Den letzten Command solltet ihr als Cronjob einrichten, um eure Datenbank sauber zu halten.
Security Best Practices für Production
Bevor ihr live geht, prüft diese Checkliste:
- HTTPS überall – OAuth2-Tokens dürfen niemals über unverschlüsselte Verbindungen übertragen werden
- Schlüssel-Rotation – Plant regelmäßige Erneuerung eurer Signing Keys
- Rate Limiting – Schützt den Token-Endpoint vor Brute-Force-Angriffen
- Monitoring – Loggt fehlgeschlagene Authentifizierungsversuche
- Token-Revocation – Implementiert einen Mechanismus zum Widerrufen kompromittierter Tokens
- Secure Storage – Speichert Refresh Tokens verschlüsselt
- CORS-Konfiguration – Beschränkt erlaubte Origins für Browser-basierte Clients
# config/packages/security.yaml
security:
access_control:
- { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED }
- { path: ^/token, roles: PUBLIC_ACCESS }
- { path: ^/api, roles: ROLE_OAUTH2_READ }
Euer Weg zur sicheren API-Authentifizierung
OAuth2 mit Symfony ist kein Hexenwerk, erfordert aber Sorgfalt bei der Implementierung. Das league/oauth2-server-bundle nimmt euch viel Arbeit ab und bietet eine solide, gewartete Grundlage.
Mit über 15 Jahren Erfahrung in Softwarequalität, Open Source und Remote Consulting unterstützen wir euch bei Never Code Alone gerne bei der Planung und Umsetzung eurer OAuth2-Infrastruktur. Ob Code Review, Architekturberatung oder Hands-on-Implementierung – meldet euch einfach.
Kontakt: roland@nevercodealone.de
Wir freuen uns auf eure Projekte und Herausforderungen.
Never Code Alone – Gemeinsam für bessere Software-Qualität!
