API-Keys, JWT und OAuth2 richtig einsetzen
Die Wahl des richtigen Authentifizierungsmechanismus entscheidet über Sicherheit und Integrationsaufwand einer PHP-API. Dieser Artikel zeigt, wann API-Keys ausreichen, wie JWT-Signaturen tatsächlich funktionieren und wann sich der zusätzliche Aufwand von OAuth2 wirklich lohnt, mit sicherer Speicherung und vollständigen Codebeispielen.
Inhaltsverzeichnis
- 1. Warum Authentifizierung eine frühe Design-Entscheidung ist
- 2. API-Keys: einfachste Form, Einsatzbereiche und Grenzen
- 3. API-Keys sicher generieren, hashen und speichern
- 4. JWT: Aufbau, Signatur und zustandslose Verifikation
- 5. JWT in PHP validieren ohne Library-Blackbox
- 6. OAuth2: Grundbegriffe und wann es wirklich gebraucht wird
- 7. Token-Refresh und Ablaufzeiten richtig gestalten
- 8. Authentifizierung und Autorisierung sauber trennen
- 9. API-Keys vs. JWT vs. OAuth2 im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Authentifizierung eine frühe Design-Entscheidung ist
Die Wahl des richtigen Mechanismus für API-Authentifizierung gehört zu den Entscheidungen, die sich in einer PHP-API später nur schwer nachträglich ändern lassen, weil jeder Client bereits gegen das gewählte Schema integriert. Wer zu früh auf einen komplexen OAuth2-Flow setzt, obwohl ein einfacher API-Key für den tatsächlichen Anwendungsfall gereicht hätte, produziert unnötigen Integrationsaufwand für alle Konsumenten. Wer umgekehrt zu lange bei simplen API-Keys bleibt, obwohl mehrere Nutzerrollen und delegierte Berechtigungen nötig sind, baut Sicherheitslücken durch improvisierte Rollenlogik.
Die drei in der Praxis dominierenden Ansätze für API-Authentifizierung sind statische API-Keys, signierte JWT-Tokens und der vollständige OAuth2-Autorisierungsrahmen. Jeder dieser Mechanismen löst ein anderes Problem, und keiner ist grundsätzlich der bessere, die richtige Wahl hängt von der Anzahl der Client-Typen, der Notwendigkeit delegierter Zugriffe und der gewünschten Kontrolle über Ablaufzeiten und Widerruf ab.
Dieser Artikel geht alle drei Mechanismen im Detail durch, mit besonderem Fokus darauf, wie sie in PHP korrekt implementiert werden, inklusive der häufigsten Sicherheitsfehler, die bei einer eigenen API-Authentifizierung entstehen, wenn Details wie Hashing, Signaturprüfung oder Ablaufzeiten übersehen werden.
2. API-Keys: einfachste Form, Einsatzbereiche und Grenzen
Ein API-Key ist die einfachste Form der API-Authentifizierung: eine lange, zufällige Zeichenkette, die der Client bei jeder Anfrage im Header mitschickt, meist als Authorization: Bearer oder in einem eigenen X-API-Key-Header. Der Server prüft den Key gegen eine gespeicherte Liste und identifiziert darüber den Aufrufer. Dieses Modell eignet sich hervorragend für Server-zu-Server-Kommunikation, bei der ein einzelner Client mit einer festen Identität arbeitet, etwa ein Partnersystem, das regelmäßig Daten synchronisiert.
Die Grenzen von API-Keys zeigen sich, sobald mehrere Nutzer über denselben Client agieren oder Zugriffsrechte zeitlich begrenzt und delegiert werden müssen. Ein API-Key hat üblicherweise keine eingebaute Ablaufzeit und keine granularen Berechtigungen, ein kompromittierter Key bleibt gültig, bis er manuell widerrufen wird. Für Endnutzer-Anwendungen mit Login-Flow ist ein API-Key daher meist die falsche Wahl, für interne Service-zu-Service-Kommunikation und einfache Partner-Integrationen bleibt er dagegen die pragmatischste Lösung.
3. API-Keys sicher generieren, hashen und speichern
Ein häufiger Fehler bei selbst gebauter API-Authentifizierung mit API-Keys: der Key wird im Klartext in der Datenbank gespeichert. Genau wie Passwörter sollten API-Keys niemals im Klartext persistiert werden, denn ein Datenbank-Leak würde sonst alle Keys sofort kompromittieren. Die Lösung ist, den Key beim Erzeugen als kryptographisch sicheren Zufallswert zu generieren, dem Client einmalig im Klartext zu zeigen, und in der Datenbank nur den Hash zu speichern.
Für den Hash reicht bei API-Keys, anders als bei Passwörtern, ein schneller kryptographischer Hash wie SHA-256 aus, weil der Key selbst bereits hohe Entropie besitzt und nicht gegen Brute-Force-Wörterbuchangriffe geschützt werden muss wie ein von Menschen gewähltes Passwort. Wichtig ist trotzdem ein zeitkonstanter Vergleich beim Prüfen des Hashes, um Timing-Angriffe zu vermeiden.
<?php
declare(strict_types=1);
/**
* Generates and verifies API keys, storing only their hash.
*/
final class ApiKeyManager
{
public function __construct(private readonly \PDO $pdo)
{
}
/**
* Generates a new key, returns the plaintext once for the client.
*/
public function generate(int $clientId): string
{
$plaintext = 'msk_' . bin2hex(random_bytes(24));
$hash = hash('sha256', $plaintext);
$stmt = $this->pdo->prepare(
'INSERT INTO api_keys (client_id, key_hash, created_at) VALUES (:client_id, :hash, NOW())'
);
$stmt->execute(['client_id' => $clientId, 'hash' => $hash]);
return $plaintext; // shown to the client exactly once
}
public function verify(string $providedKey): ?int
{
$hash = hash('sha256', $providedKey);
$stmt = $this->pdo->prepare(
'SELECT client_id FROM api_keys WHERE key_hash = :hash AND revoked_at IS NULL'
);
$stmt->execute(['hash' => $hash]);
$clientId = $stmt->fetchColumn();
return $clientId !== false ? (int) $clientId : null;
}
}
4. JWT: Aufbau, Signatur und zustandslose Verifikation
JWT, JSON Web Token, löst ein Problem, das API-Keys nicht abdecken: zustandslose API-Authentifizierung ohne Datenbankabfrage bei jeder Anfrage. Ein JWT besteht aus drei Base64-URL-kodierten Teilen, getrennt durch Punkte: Header mit Algorithmus-Angabe, Payload mit den eigentlichen Claims wie Nutzer-ID und Ablaufzeit, und Signatur. Die Signatur wird über Header und Payload mit einem geheimen Schlüssel berechnet, sodass jede Manipulation am Inhalt die Signaturprüfung fehlschlagen lässt.
Der entscheidende Vorteil gegenüber API-Keys: der Server muss den Token nicht in einer Datenbank nachschlagen, die Signaturprüfung allein reicht aus, um Authentizität und Unversehrtheit zu bestätigen. Das macht JWT besonders attraktiv für Systeme mit mehreren zustandslosen Backend-Instanzen, die keine gemeinsame Session-Datenbank teilen wollen. Der Nachteil: ein einmal ausgestellter Token lässt sich vor Ablauf nicht ohne zusätzliche Infrastruktur wie eine Sperrliste widerrufen.
{
"header": { "alg": "HS256", "typ": "JWT" },
"payload": {
"sub": "user-4821",
"role": "customer",
"iat": 1785484800,
"exp": 1785488400
}
}
5. JWT in PHP validieren ohne Library-Blackbox
Auch wenn produktive PHP-APIs für JWT-Verarbeitung meist eine geprüfte Library wie firebase/php-jwt einsetzen sollten, hilft ein Blick auf die manuelle Implementierung, um zu verstehen, was diese Libraries tatsächlich tun. Die Validierung besteht aus drei Schritten: Header und Payload dekodieren, die Signatur mit dem geheimen Schlüssel neu berechnen und mit der übermittelten Signatur zeitkonstant vergleichen, und schließlich die Ablaufzeit im exp-Claim prüfen.
Ein häufiger Sicherheitsfehler bei selbst gebauter JWT-Verifikation: der Algorithmus aus dem Header wird blind übernommen, statt fest im Server-Code vorzugeben, welcher Algorithmus erwartet wird. Ein Angreifer könnte sonst den Algorithmus im Header auf none setzen und eine unsignierte Nutzlast einschleusen, ein real dokumentierter Angriff gegen unsauber implementierte JWT-Bibliotheken.
<?php
declare(strict_types=1);
/**
* Minimal, educational JWT verification for HS256 tokens.
* Production code should use a vetted library instead.
*/
final class JwtVerifier
{
public function __construct(private readonly string $secret)
{
}
/**
* @return array<string, mixed> Decoded payload
* @throws \RuntimeException On invalid signature or expired token
*/
public function verify(string $token): array
{
[$headerB64, $payloadB64, $signatureB64] = explode('.', $token) + [null, null, null];
if ($headerB64 === null || $payloadB64 === null || $signatureB64 === null) {
throw new \RuntimeException('Malformed token.');
}
// Algorithm is fixed here, never trusted from the token header
$expectedSignature = hash_hmac('sha256', "{$headerB64}.{$payloadB64}", $this->secret, true);
$expectedB64 = rtrim(strtr(base64_encode($expectedSignature), '+/', '-_'), '=');
if (!hash_equals($expectedB64, $signatureB64)) {
throw new \RuntimeException('Invalid signature.');
}
$payload = json_decode(base64_decode(strtr($payloadB64, '-_', '+/')), true);
if (($payload['exp'] ?? 0) < time()) {
throw new \RuntimeException('Token expired.');
}
return $payload;
}
}
6. OAuth2: Grundbegriffe und wann es wirklich gebraucht wird
OAuth2 ist kein Authentifizierungsprotokoll im engeren Sinn, sondern ein Autorisierungsrahmen für delegierten Zugriff: eine Anwendung erhält Zugriff auf Ressourcen im Namen eines Nutzers, ohne dessen Passwort jemals zu sehen. Zentrale Begriffe sind der Resource Owner, der Client, der Authorization Server und der Resource Server. Der Client erhält nach erfolgreicher Autorisierung ein Access Token, häufig als JWT umgesetzt, und optional ein Refresh Token für die langfristige Erneuerung.
OAuth2 lohnt sich fast ausschließlich dann, wenn tatsächlich Drittanbieter-Anwendungen im Namen von Nutzern auf eine API zugreifen sollen, etwa wenn externe Partner-Apps Bestellungen im Namen eines Shop-Kunden abrufen dürfen. Für eine interne API, bei der Client und Server demselben Unternehmen gehören, ist der Implementierungsaufwand von OAuth2 in den meisten Fällen unverhältnismäßig hoch, hier reichen einfachere Mechanismen wie API-Keys oder direkt ausgestellte JWTs völlig aus.
7. Token-Refresh und Ablaufzeiten richtig gestalten
Kurze Ablaufzeiten für Access Tokens reduzieren den Schaden bei einem gestohlenen Token erheblich, erzeugen aber häufigere Anfragen zum Erneuern. Die etablierte Lösung: kurzlebige Access Tokens, typischerweise 15 Minuten, kombiniert mit langlebigen Refresh Tokens, die ausschließlich für den Refresh-Endpunkt gültig sind und serverseitig widerrufbar bleiben, weil sie im Gegensatz zu Access Tokens in einer Datenbank gespeichert werden.
Ein Refresh Token sollte bei jeder Nutzung rotiert werden, das heißt, ein neues Refresh Token wird ausgestellt und das alte sofort ungültig gemacht. Wird ein bereits verwendetes, altes Refresh Token erneut vorgelegt, deutet das auf einen gestohlenen Token hin, und die API sollte in diesem Fall sicherheitshalber die gesamte Token-Familie widerrufen, statt nur die einzelne Anfrage abzulehnen.
8. Authentifizierung und Autorisierung sauber trennen
Ein häufiger Designfehler vermengt Authentifizierung, die Frage "wer ist der Aufrufer", mit Autorisierung, der Frage "was darf dieser Aufrufer tun". API-Keys, JWT und OAuth2 lösen ausschließlich das erste Problem, die Identifikation des Aufrufers. Rollen, Berechtigungen und ressourcenspezifische Zugriffsregeln gehören in eine separate Autorisierungsschicht, die nach erfolgreicher Authentifizierung greift und unabhängig vom gewählten Authentifizierungsmechanismus austauschbar bleibt.
Diese Trennung zahlt sich vor allem dann aus, wenn sich der Authentifizierungsmechanismus später ändert, etwa eine Migration von API-Keys zu OAuth2. Bleibt die Autorisierungslogik als eigene Schicht bestehen, betrifft die Migration ausschließlich, wie die Identität des Aufrufers ermittelt wird, nicht, welche Berechtigungen daraus abgeleitet werden.
9. API-Keys vs. JWT vs. OAuth2 im Vergleich
Die folgende Tabelle vergleicht die drei Mechanismen entlang der wichtigsten Entscheidungskriterien.
| Kriterium | API-Key | JWT | OAuth2 |
|---|---|---|---|
| Implementierungsaufwand | Sehr gering | Moderat | Hoch |
| Zustandslose Prüfung | Nein, DB-Abfrage nötig | Ja, nur Signaturprüfung | Ja, Access Token wie JWT |
| Delegierter Nutzerzugriff | Nicht vorgesehen | Nur mit zusätzlicher Logik | Kernfunktion |
| Widerruf vor Ablauf | Einfach per DB-Flag | Nur mit Sperrliste | Über Refresh-Token-Widerruf |
| Typischer Einsatz | Server-zu-Server, Partner | Eigene Mobile/Web-Clients | Drittanbieter-Integrationen |
In der Praxis kombinieren viele PHP-APIs mehrere Mechanismen: API-Keys für Server-zu-Server-Integrationen, JWT für den eigenen mobilen Client, und OAuth2 ausschließlich für die Handvoll Drittanbieter-Partner, die tatsächlich delegierten Zugriff benötigen.
Mironsoft
PHP-API-Security und Authentifizierungsarchitektur
Die richtige Authentifizierung für eure PHP-API?
Wir analysieren euren Anwendungsfall, wählen zwischen API-Keys, JWT und OAuth2 die passende Lösung und setzen sichere Speicherung, Signaturprüfung und Token-Refresh produktionsreif um.
Security-Audit
Bestehende Authentifizierungslogik auf Schwachstellen prüfen
JWT/OAuth2-Einführung
Signaturprüfung, Ablaufzeiten und Refresh-Flows sauber implementieren
Autorisierungsschicht
Rollen und Berechtigungen sauber von der Authentifizierung trennen
10. Zusammenfassung
API-Authentifizierung in PHP ist keine Ein-Größe-passt-allen-Entscheidung: API-Keys bleiben die pragmatische Wahl für Server-zu-Server-Kommunikation mit fester Identität, JWT bringt zustandslose Verifikation für eigene mobile und Web-Clients, und OAuth2 lohnt sich fast ausschließlich für delegierten Zugriff durch echte Drittanbieter. Wer diese drei Mechanismen nach ihrem tatsächlichen Einsatzzweck statt nach Trend wählt, vermeidet sowohl unnötigen Implementierungsaufwand als auch unpassende Sicherheitslücken.
Unabhängig vom gewählten Mechanismus gelten dieselben Grundregeln: Keys und Secrets niemals im Klartext speichern, Signaturen immer zeitkonstant vergleichen, den erwarteten Algorithmus fest im Server-Code verankern statt aus dem Token zu übernehmen, und Authentifizierung strikt von Autorisierung trennen. Diese Grundlagen entscheiden am Ende mehr über die Sicherheit einer PHP-API als die Wahl zwischen API-Key, JWT und OAuth2 selbst.
API-Authentifizierung in PHP: Das Wichtigste auf einen Blick
API-Keys
Nur gehasht speichern, ideal für Server-zu-Server, keine granularen Berechtigungen oder Ablaufzeit eingebaut.
JWT
Zustandslose Signaturprüfung statt DB-Abfrage, Algorithmus immer serverseitig fest vorgeben, nie aus dem Header übernehmen.
OAuth2
Nur bei echtem delegiertem Drittanbieter-Zugriff einsetzen, sonst unverhältnismäßig hoher Aufwand.
Refresh-Strategie
Kurzlebige Access Tokens plus rotierende, widerrufbare Refresh Tokens für den richtigen Kompromiss aus Sicherheit und Nutzbarkeit.