Firebase Auth + Kendi PHP Sunucun: Composer Olmadan Token Doğrulama ve Hesap Silme
Anılog'u geliştirirken verdiğim en belirleyici karar, Firebase'i yalnızca kimlik için kullanmaktı. Gruplar, klipler, yorumlar, medya dosyaları — hepsi kendi Hostinger sunucumda, saf PHP ve MySQL üzerinde.
Bu ayrımın bir bedeli var: sunucunun, istemcinin gönderdiği Firebase kimlik jetonunun gerçekten Google tarafından imzalandığını kendi başına doğrulaması gerekiyor. Paylaşımlı hostingte composer çalıştıramadığım için firebase/php-jwt gibi hazır bir kütüphane de yoktu.
Bu yazıda iki şeyin tam kurulumunu anlatıyorum:
- Bağımlılıksız PHP ile Firebase kimlik jetonu doğrulamak
- Mağazaların zorunlu tuttuğu hesap silme akışını, hem uygulamada hem web üzerinden çalışır hâlde kurmak
Bölüm 1: Jetonu doğrulamak
Firebase'in verdiği kimlik jetonu, RS256 ile imzalanmış bir JWT. Doğrulama üç adım:
- İmzayı Google'ın açık anahtarıyla kontrol et
- Hedef kitle (
aud) ve yayıncının (iss) senin projene ait olduğunu kontrol et - Süresinin dolmadığını kontrol et
PHP'nin openssl eklentisi üçü için de yeterli.
Google'ın sertifikalarını almak
Google, jeton imzalarını doğrulamak için kullanılan x509 sertifikalarını sabit bir adreste yayımlıyor:
const GOOGLE_CERT_URL =
'https://www.googleapis.com/robot/v1/metadata/x509/[email protected]';Bu sertifikalar düzenli olarak dönüyor, ama her istekte indirmek saçma olur — her API çağrısına bir dış HTTP isteği eklemek demek. Yanıtı diske önbelleklemek ve Cache-Control başlığındaki süreye saygı duymak gerekiyor.
İmzayı doğrulamak
Kritik kısım şu üç satır:
[$h, $p, $s] = explode('.', $jwt);
$header = json_decode(b64url_decode($h), true);
if (($header['alg'] ?? '') !== 'RS256') {
fail('Desteklenmeyen imza türü.', 401, 'token_bozuk');
}
$certs = google_certs();
$pem = $certs[$header['kid']] ?? null;
if ($pem === null) fail('Bilinmeyen anahtar.', 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('İmza doğrulanamadı.', 401, 'token_bozuk');İmzalanan metin "$h.$p" — yani noktayla birlikte başlık ve gövdenin base64url hâli. Bunları çözüp yeniden birleştirmek imzayı bozar; ham hâlleriyle kullanmak gerekiyor.
alg alanını kontrol etmek isteğe bağlı değil. Kontrol etmezseniz saldırgan alg: none gönderip imzasız bir jetonu kabul ettirebilir — JWT'nin en bilinen açığı bu.
Talepleri doğrulamak
İmza doğru olması tek başına yetmez. Jeton başka bir Firebase projesinden gelmiş olabilir:
$iss = 'https://securetoken.google.com/' . FIREBASE_PROJECT_ID;
if (($claims['iss'] ?? '') !== $iss) fail('Geçersiz yayıncı.', 401);
if (($claims['aud'] ?? '') !== FIREBASE_PROJECT_ID) fail('Geçersiz hedef.', 401);
if (($claims['exp'] ?? 0) < time()) fail('Süresi dolmuş.', 401);aud kontrolünü atlamak, herhangi bir Firebase projesinden alınmış bir jetonun sizin API'nizde geçerli olması demek. Sık yapılan bir hata.
Kullanıcıyı eşlemek
Doğrulama geçtikten sonra elinizde sub alanında Firebase UID var. Kendi users tablonuzda bu UID'yi taşıyan bir satır tutuyorsunuz; ilk girişte oluşturuluyor.
Buradaki incelik: e-postayı birincil anahtar yapmayın. Apple ile giriş "E-postamı Gizle" seçeneğini destekliyor ve kullanıcı bunu sonradan değiştirebiliyor. Kimliğin tek sabit noktası UID.
Bölüm 2: Hesap silme
Hem App Store hem Play Store, hesap oluşturulabilen uygulamalarda hesabın silinebilmesini zorunlu tutuyor. Play Store bir adım daha ileri gidiyor: silme işlemi uygulamayı kurmadan da, bir web adresinden yapılabilmeli.
Yani iki ayrı yol kurmak gerekiyor.
Yol 1: Uygulama içinden
Kullanıcı Profil → Hesabı sil diyor. Sunucudaki verisi ve Firebase'deki kimliği siliniyor.
Buradaki sıra kritik. İlk sürümümde önce sunucu verisini siliyor, sonra Firebase kullanıcısını silmeye çalışıyordum. Firebase çağrısı requires-recent-login hatasıyla düşünce ortaya şu durum çıkıyordu: verisi silinmiş ama hâlâ giriş yapmış bir kullanıcı.
Doğru sıra:
Future<void> hesabiSil() async {
// Önce kimliği tazele: silme işlemi yakın zamanlı giriş istiyor.
await _yenidenDogrula();
await _api.delete('/me');
try {
await _auth.currentUser?.delete();
} finally {
// Firebase silme başarısız olsa bile oturum kapanmalı; sunucudaki
// veri gitti, kullanıcı içeride kalmamalı.
await cikisYap();
}
}finally bloğu önemli: sunucu verisi silindiyse, Firebase tarafı ne olursa olsun kullanıcı dışarı çıkarılmalı.
Yol 2: Web üzerinden, uygulama olmadan
Bu daha zor kısım. Web sayfası, kimliği doğrulanmamış bir ziyaretçiden gelen "şu hesabı sil" isteğini nasıl güvenle işleyecek?
Kurduğum akış iki adımlı:
- Ziyaretçi e-postasını giriyor. Sunucu, o e-postaya ait bir hesap varsa altı haneli bir kod üretip e-posta olarak gönderiyor.
- Ziyaretçi kodu giriyor. Kod doğruysa silme anında gerçekleşiyor — elle onay kuyruğu yok.
Sunucu tarafında dikkat edilen noktalar:
- Kod hash'lenerek saklanıyor (HMAC-SHA256). Veritabanını gören biri kodu okuyamıyor.
- Hesap var mı bilgisi sızdırılmıyor. E-posta kayıtlı olsun ya da olmasın yanıt aynı, süresi bile eşitleniyor. Aksi hâlde form bir "bu e-posta kayıtlı mı" sorgulama aracına dönüşür.
- Hız sınırı var: aynı e-posta ve aynı IP için deneme sayısı sınırlı.
- Silme işlemi
FOR UPDATEsatır kilidiyle yapılıyor; aynı anda iki istek gelirse ikisi de çalışmıyor.
Firebase kullanıcısını sunucudan silmek
Web akışında kullanıcının oturumu yok, dolayısıyla istemci tarafı currentUser.delete() çağrısı mümkün değil. Firebase Admin yetkisiyle sunucudan silmek gerekiyor. Bunun için de composer yok.
Zincir üç adım:
servis hesabı anahtarı (JSON)
→ RS256 ile imzalanmış JWT üret
→ oauth2.googleapis.com/token'dan erişim jetonu al
→ identitytoolkit.googleapis.com üzerinde accounts:lookup / accounts:deleteJWT'yi kendiniz imzalıyorsunuz — bu sefer doğrulama değil imzalama tarafındasınız:
$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,
];Sonra accounts:lookup ile e-postadan UID'yi buluyor, accounts:delete ile siliyorsunuz.
Servis hesabı anahtarı hakkında bir uyarı: bu JSON dosyası projenizin tam yetkisi demek. Web'den erişilemeyecek bir klasörde durmalı ve o klasörde Require all denied içeren bir .htaccess bulunmalı. Kurduktan sonra dosyanın adresini tarayıcıda açıp 403 aldığınızı doğrulayın. Ben doğruladım; doğrulamasaydım fark etmezdim.
Bu kurulumun bedeli ve kazancı
Bedeli: hazır SDK'nın yaptığı işi kendiniz yazıyorsunuz. Yaklaşık 200 satır PHP, ve doğrulama mantığında yapılacak bir hata doğrudan güvenlik açığı.
Kazancı: medya ve veri kendi sunucunuzda, maliyet öngörülebilir, satıcıya bağımlılık kimlikle sınırlı. Video barındıran bir uygulamada bu fark küçük değil.
Anılog'un genel mimarisini ve bu kararın neden verildiğini ayrı bir yazıda anlattım.
İlgili Yazılar
Anılog'u Nasıl Geliştirdim: Fikirden Mağazaya Bir Uygulamanın Karar Günlüğü
Arkadaş grupları için günlük klip uygulaması Anılog'u sıfırdan kurarken verdiğim mimari kararları anlatıyorum: kimlik neden Firebase'de, veri neden kendi sunucumda, isim nasıl seçildi ve hangi kararlar geri döndürülemez çıktı.
Flutter'da Kamerayı Kullanıcıya Bırakmak: Yön Zorlamadan Video Çekmek
Flutter camera paketinde önizlemenin ters dönmesi, kayıt başlarken donma ve bildirim merkezi açılınca kameranın kilitlenmesi. Anılog'u geliştirirken çözdüğüm üç somut hatayı ve yön zorlamadan çalışan kamera ekranının kodunu anlatıyorum.
ffmpeg ile Telefonda Video Birleştirme: Farklı Oranlardaki Klipleri Tek Vloga Çevirmek
Kimi dikey kimi yatay, kimi video kimi fotoğraf. Anılog'da bu karışımı sunucu kullanmadan, telefonda tek bir 1920x1080 vloga çeviren ffmpeg zincirini adım adım anlatıyorum: bulanık dolgu, Türkçe karakterli yazı, müzik ve ölçülmüş sıkıştırma.