Firebase Auth + Kendi PHP Sunucun: Composer Olmadan Token Doğrulama ve Hesap Silme
Son güncelleme:
5 dk okuma

Recapday'i 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. Firebase'in kimlik jetonu doğrulama belgesi, Admin SDK'nın desteklemediği dillerde üçüncü taraf bir JWT kütüphanesiyle nasıl ilerleneceğini de anlatıyor. 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'in Flutter kullanıcı yönetimi belgesinde belirtildiği gibi silme işlemi yakın zamanlı giriş istiyor; 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'in sunucu tarafı kullanıcı yönetimi belgelerinde anlatıldığı gibi 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.
Recapday'in genel mimarisini ve bu kararın neden verildiğini ayrı bir yazıda anlattım.
Sık Sorulan Sorular
Firebase Admin SDK olmadan kimlik jetonu doğrulanabilir mi?
Evet. Jeton RS256 ile imzalanmış bir JWT olduğu için üçüncü taraf bir JWT kütüphanesi ya da doğrudan PHP'nin openssl eklentisi yeterli oluyor. Admin SDK'nın desteklemediği dillerde izlenen yol da bu.
Doğrulamada tam olarak neye bakılıyor?
Üç şeye: imzanın Google'ın açık anahtarıyla doğrulanması, aud ve iss alanlarının sizin projenize ait olması, ve jetonun süresinin dolmamış olması. Üçünden biri tutmazsa istek reddedilir.
Hesap silme neden iki ayrı yol istiyor?
App Store ve Play Store, hesap oluşturulabilen uygulamalarda hesabın silinebilmesini zorunlu tutuyor. Play Store bir adım daha ileri gidip silmenin uygulamayı kurmadan, bir web adresinden de yapılabilmesini istiyor. Bu yüzden hem uygulama içi hem web tarafı gerekiyor.
Silme sırasında hangi sıra izlenmeli?
Önce kimliği tazeleyin, sonra sunucudaki veriyi silin, en son Firebase kullanıcısını silin. Ters sırada Firebase requires-recent-login hatasıyla düştüğünde ortada verisi silinmiş ama hâlâ giriş yapmış bir kullanıcı kalıyor.
İlgili Yazılar
Recapday'i Nasıl Geliştirdim: Fikirden Mağazaya Bir Uygulamanın Karar Günlüğü
Recapday'i kurarken verdiğim mimari kararlar: kimlik neden Firebase'de, veri neden kendi sunucumda ve hangi kararlar geri döndürülemez çıktı.
ffmpeg ile Telefonda Video Birleştirme: Farklı Oranlardaki Klipleri Tek Vloga Çevirmek
Dikey, yatay ve fotoğraf kliplerini telefonda tek bir 1920x1080 vloga çeviren ffmpeg zincirini anlatıyorum: bulanık dolgu, yazı, müzik ve sıkıştırma.
Tiny Glade Mobil Var mı? İndirme ve Telefonda Oynama
Tiny Glade’in resmî iPhone ya da Android sürümü yok. Oyunu nereden güvenle indireceğini, telefonda oynamanın iki yolunu ve mobilde benzer oyunları anlattım.