İçeriğe geç / Skip to content / Zum Inhalt

Firebase Auth mit eigenem PHP-Server: Token-Prüfung und Kontolöschung ohne Composer

Ahmet Balaman

Zuletzt aktualisiert:

5 Min. Lesezeit

MobilFirebasePHPAuthentifizierungJWTMySQLRecapday
Firebase Auth mit eigenem PHP-Server: Token-Prüfung und Kontolöschung ohne Composer

Die folgenreichste Entscheidung beim Bau von Recapday war, Firebase nur für die Identität zu nutzen. Gruppen, Clips, Kommentare, Mediendateien — alles liegt auf meinem eigenen Hostinger-Server, auf reinem PHP und MySQL.

Diese Trennung hat einen Preis: der Server muss selbst prüfen, dass das vom Client gesendete Firebase-ID-Token wirklich von Google signiert wurde. Und da ich auf Shared Hosting kein Composer ausführen kann, gab es auch kein firebase/php-jwt.

Dieser Beitrag zeigt zwei Dinge vollständig:

  1. Ein Firebase-ID-Token in abhängigkeitsfreiem PHP prüfen
  2. Den von den Stores geforderten Kontolöschungs-Ablauf aufbauen — in der App und im Web

Teil 1: das Token prüfen

Ein Firebase-ID-Token ist ein mit RS256 signiertes JWT. Die Firebase-Anleitung zum Prüfen von ID-Token beschreibt auch, wie man in Sprachen ohne Admin-SDK mit einer fremden JWT-Bibliothek vorgeht. Die Prüfung besteht aus drei Schritten:

  • Die Signatur gegen Googles öffentlichen Schlüssel prüfen
  • Prüfen, dass Empfänger (aud) und Aussteller (iss) zum eigenen Projekt gehören
  • Prüfen, dass es nicht abgelaufen ist

Die App erhält von Firebase ein signiertes idToken, sendet es an den eigenen Server, der Signatur, aud, iss und exp prüft, bevor er handelt

PHPs openssl-Erweiterung deckt alle drei ab.

Googles Zertifikate holen

Google veröffentlicht die x509-Zertifikate zur Signaturprüfung unter einer festen Adresse:

const GOOGLE_CERT_URL =
    'https://www.googleapis.com/robot/v1/metadata/x509/[email protected]';

Sie rotieren regelmäßig, aber sie bei jeder Anfrage zu laden wäre absurd — das wäre ein ausgehender HTTP-Aufruf pro API-Zugriff. Die Antwort gehört auf die Platte zwischengespeichert, mit Respekt vor der Lebensdauer im Cache-Control-Header.

Die Signatur prüfen

Der entscheidende Teil sind diese Zeilen:

[$h, $p, $s] = explode('.', $jwt);

$header = json_decode(b64url_decode($h), true);
if (($header['alg'] ?? '') !== 'RS256') {
    fail('Nicht unterstützter Signaturtyp.', 401, 'token_bozuk');
}

$certs = google_certs();
$pem = $certs[$header['kid']] ?? null;
if ($pem === null) fail('Unbekannter Schlüssel.', 401, 'token_bozuk');

$pub = openssl_pkey_get_public($pem);
$valid = openssl_verify("$h.$p", b64url_decode($s), $pub, OPENSSL_ALGO_SHA256);
if ($valid !== 1) fail('Signaturprüfung fehlgeschlagen.', 401, 'token_bozuk');

Signiert wird der Text "$h.$p" — Header und Payload in base64url, mit dem Punkt dazwischen. Sie zu dekodieren und neu zu kodieren zerstört die Signatur; man muss die rohen Zeichenketten verwenden.

Die Prüfung von alg ist nicht optional. Ohne sie kann ein Angreifer alg: none senden und ein unsigniertes Token akzeptieren lassen — die bekannteste JWT-Schwachstelle überhaupt.

Die Claims prüfen

Eine gültige Signatur allein genügt nicht. Das Token könnte aus einem anderen Firebase-Projekt stammen:

$iss = 'https://securetoken.google.com/' . FIREBASE_PROJECT_ID;

if (($claims['iss'] ?? '') !== $iss)                fail('Falscher Aussteller.', 401);
if (($claims['aud'] ?? '') !== FIREBASE_PROJECT_ID) fail('Falscher Empfänger.', 401);
if (($claims['exp'] ?? 0) < time())                 fail('Abgelaufen.', 401);

Die aud-Prüfung auszulassen heißt, dass ein Token aus irgendeinem Firebase-Projekt gegen die eigene API gültig ist. Ein häufiger Fehler.

Auf den eigenen Nutzer abbilden

Nach bestandener Prüfung steht die Firebase-UID in sub. In der eigenen users-Tabelle liegt eine Zeile mit dieser UID, angelegt bei der ersten Anmeldung.

Die Feinheit: nicht die E-Mail als Schlüssel nehmen. "Anmelden mit Apple" unterstützt "E-Mail verbergen", und sie kann sich später ändern. Der einzige stabile Anker ist die UID.

Teil 2: Kontolöschung

App Store und Play Store verlangen beide, dass eine App, in der man ein Konto anlegen kann, es auch löschen lässt. Play geht einen Schritt weiter: die Löschung muss ohne Installation der App über eine Webadresse möglich sein.

Man braucht also zwei getrennte Wege.

Weg 1: aus der App heraus

Profil → Konto löschen. Serverdaten und Firebase-Identität werden entfernt.

Die Reihenfolge ist entscheidend. In meiner ersten Fassung löschte ich erst die Serverdaten und versuchte dann, den Firebase-Nutzer zu löschen. Wie die Firebase-Dokumentation zur Nutzerverwaltung in Flutter festhält, verlangt das Löschen eine kürzlich erfolgte Anmeldung; scheiterte dieser Aufruf mit requires-recent-login, blieb ein Zustand übrig: Daten weg, aber weiterhin angemeldet.

Die richtige Reihenfolge:

Future<void> hesabiSil() async {
  // Zuerst die Anmeldung auffrischen: Löschen verlangt einen frischen Login.
  await _yenidenDogrula();

  await _api.delete('/me');

  try {
    await _auth.currentUser?.delete();
  } finally {
    // Auch wenn die Firebase-Löschung scheitert: abmelden. Die Serverdaten
    // sind weg, niemand darf drinnen bleiben.
    await cikisYap();
  }
}

Der finally-Block ist der Punkt: sind die Serverdaten gelöscht, geht die Person hinaus, egal was Firebase tut.

Weg 2: im Web, ohne App

Das ist die schwierigere Hälfte. Wie handelt eine Webseite sicher auf ein "lösche dieses Konto" von einer nicht angemeldeten Person?

Der Ablauf hat zwei Schritte:

  1. Die Person gibt ihre E-Mail ein. Existiert ein Konto, erzeugt der Server einen sechsstelligen Code und schickt ihn per E-Mail.
  2. Die Person gibt den Code ein. Stimmt er, erfolgt die Löschung sofort — keine manuelle Freigabeschlange.

Worauf die Serverseite achtet:

  • Der Code wird gehasht gespeichert (HMAC-SHA256). Wer die Datenbank sieht, liest den Code nicht.
  • Ob ein Konto existiert, wird nie verraten. Die Antwort ist identisch, bis hin zur angeglichenen Antwortzeit. Sonst wird das Formular zum Werkzeug, um registrierte Adressen abzufragen.
  • Ratenbegrenzung pro E-Mail und pro IP.
  • Die Löschung läuft unter einer FOR UPDATE-Zeilensperre; zwei gleichzeitige Anfragen können nicht beide durchgehen.

Den Firebase-Nutzer vom Server aus löschen

Im Web-Ablauf gibt es keine Sitzung, ein clientseitiges currentUser.delete() fällt also aus. Man muss mit Firebase-Admin-Rechten vom Server löschen, so wie es die Firebase-Dokumentation zur serverseitigen Nutzerverwaltung beschreibt. Und wieder ohne Composer.

Die Kette hat drei Schritte:

Dienstkonto-Schlüssel (JSON)
  → ein mit RS256 signiertes JWT bauen
  → es bei oauth2.googleapis.com/token gegen ein Zugriffstoken tauschen
  → accounts:lookup / accounts:delete auf identitytoolkit.googleapis.com aufrufen

Diesmal signiert man das JWT selbst, statt eines zu prüfen:

$jwt_govde = [
    'iss'   => $sa['client_email'],
    'scope' => 'https://www.googleapis.com/auth/identitytoolkit',
    'aud'   => $sa['token_uri'] ?? 'https://oauth2.googleapis.com/token',
    'iat'   => time(),
    'exp'   => time() + 3600,
];

Danach macht accounts:lookup aus der E-Mail eine UID, und accounts:delete entfernt sie.

Eine Warnung zum Dienstkonto-Schlüssel: diese JSON-Datei ist die volle Kontrolle über das Projekt. Sie gehört in ein Verzeichnis, das das Web nicht erreicht, mit einer .htaccess, die Require all denied enthält. Danach die Adresse der Datei im Browser öffnen und den 403 bestätigen. Ich habe das bestätigt; sonst hätte ich es nicht gewusst.

Was dieses Vorgehen kostet und bringt

Kosten: man schreibt, was ein SDK geschrieben hätte. Rund 200 Zeilen PHP — und ein Fehler in der Prüflogik ist eine Sicherheitslücke, kein Bug.

Nutzen: Medien und Daten auf dem eigenen Server, vorhersehbare Kosten, Herstellerbindung nur bei der Identität. Bei einer App, die Videos hostet, ist dieser Unterschied nicht klein.

Die Gesamtarchitektur von Recapday und den Grund für diese Entscheidung habe ich in einem eigenen Beitrag beschrieben.

Häufig gestellte Fragen

Lässt sich ein ID-Token ohne das Firebase Admin SDK prüfen?

Ja. Das Token ist ein mit RS256 signiertes JWT, deshalb genügt eine JWT-Bibliothek eines Drittanbieters oder die openssl-Erweiterung von PHP. In Sprachen, die das Admin SDK nicht unterstützt, geht man genau diesen Weg.

Was wird bei der Prüfung genau kontrolliert?

Drei Dinge: dass die Signatur gegen Googles öffentlichen Schlüssel gültig ist, dass aud und iss zu Ihrem Projekt gehören, und dass das Token nicht abgelaufen ist. Scheitert eines davon, wird die Anfrage abgelehnt.

Warum braucht die Kontolöschung zwei getrennte Wege?

App Store und Play Store verlangen bei Apps mit Kontoerstellung, dass das Konto gelöscht werden kann. Der Play Store geht weiter und verlangt, dass die Löschung auch ohne Installation über eine Webadresse möglich ist — deshalb braucht es beide Wege.

In welcher Reihenfolge sollte gelöscht werden?

Zuerst die Anmeldung auffrischen, dann die Daten auf dem eigenen Server löschen, zuletzt den Firebase-Nutzer. In umgekehrter Reihenfolge bleibt bei einem requires-recent-login-Fehler ein Nutzer übrig, dessen Daten weg sind, der aber noch angemeldet ist.

Kommentare