Ders 02 / 23
Temel Kimlik Doğrulama
HTTP'nin kendi içinde tanımlı temel şemasının kurulması: 401 daveti ve WWW-Authenticate başlığı, taşınan değerin çözülebilir bir kodlama olduğunun gösterilmesi, sabit zamanlı karşılaştırma ve kimlik bilgisinin her istekte geçmesinin sonuçları.
İçindekiler
Önceki derste kimlik, bir eşleme tablosunun arkasına saklanmıştı: istek bir dizge taşıdı,
sunucu onu bir asıla çevirdi. O dizgenin nereden geldiği sorusu açık kalmıştı. HTTP bu soruya
kendi içinde bir yanıt verir ve yanıtın en küçüğü temel şemadır (Basic authentication
scheme): kullanıcı adı ile parola, her istekte Authorization başlığında taşınır.
Şema küçüktür ve tam olarak bu yüzden öğreticidir. Kimlik doğrulamanın bütün parçalarını görünür kılar: sunucunun kimlik bilgisini nasıl istediği, istemcinin onu nasıl yolladığı, sunucunun nasıl karşılaştırdığı ve kimlik bilgisinin ne kadar süre ortalıkta durduğu. Sonraki derslerin çözdüğü sorunların çoğu, bu şemanın çözmediği sorunlardır.
Şemanın İki Adımı
Şema bir davetle başlar. İstek kimlik bilgisi taşımıyorsa sunucu 401 döner ve
WWW-Authenticate başlığında hangi şemayı beklediğini bildirir. Başlıktaki realm
parametresi, kimlik bilgisinin geçerli olduğu bölgeyi adlandırır; aynı sunucuda birden
çok bölge bulunabilir ve istemci hangi bölge için hangi bilgiyi kullanacağını bu adla ayırır.
charset="UTF-8" parametresi ise kullanıcı adı ve parolanın hangi kodlamayla baytlara
çevrileceğini söyler.
İkinci adımda istemci Authorization: Basic <değer> başlığıyla döner. Değer,
kullanıcı:parola dizgesinin base64 kodlamasıdır.
// temel.mjs — HTTP temel kimlik dogrulama semasi (RFC 7617) // Kullanim: node temel.mjs <port> import { createServer } from "node:http"; import { timingSafeEqual } from "node:crypto"; const PORT = Number(process.argv[2] ?? 8511); // Gercek kurulumda parola dogrudan degil, dogrulayici olarak saklanir; // bu ders semayi anlatir, saklamayi degil. const HESAPLAR = new Map([ ["raf-terminali", "Kt-9f3a-Raf!"], ["deniz.aral", "üç-kitap-1876"], ]); // Sabit zamanli karsilastirma: esitsizlik kadar uzunluk farki da sizdirmasin const esitMi = (a, b) => { const x = Buffer.from(a, "utf8"), y = Buffer.from(b, "utf8"); return x.length === y.length && timingSafeEqual(x, y); }; let istekSayaci = 0, parolaGorulmesi = 0; createServer((istek, yanit) => { yanit.sendDate = false; istekSayaci++; const json = (kod, govde, baslik = {}) => { yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8", ...baslik }); yanit.end(JSON.stringify(govde)); }; const reddet = () => json(401, { hata: "kimlik dogrulanamadi" }, { "www-authenticate": 'Basic realm="odunc", charset="UTF-8"' }); const baslik = istek.headers.authorization ?? ""; if (!baslik.startsWith("Basic ")) return reddet(); // Basic <base64(kullanici:parola)> — cozme icin anahtar gerekmez const cozulmus = Buffer.from(baslik.slice(6), "base64").toString("utf8"); const ayrac = cozulmus.indexOf(":"); if (ayrac < 0) return reddet(); const kullanici = cozulmus.slice(0, ayrac), parola = cozulmus.slice(ayrac + 1); parolaGorulmesi++; // parola bu istekte de sunucuya ulasti const beklenen = HESAPLAR.get(kullanici); if (beklenen === undefined || !esitMi(parola, beklenen)) return reddet(); if (istek.url === "/sayac") return json(200, { istek: istekSayaci, parolaGorulmesi }); json(200, { kullanici, kaynak: istek.url }); }).listen(PORT, "127.0.0.1", () => console.log(`temel 127.0.0.1:${PORT}`));
node temel.mjs 8511 > /dev/null & SUNUCU=$! sleep 1 echo "--- kimlik bilgisi yok" curl -s -D - -o /dev/null http://127.0.0.1:8511/odunc/O-1 | head -3 echo "--- curl -u ile" curl -s -w " %{http_code}\n" -u "raf-terminali:Kt-9f3a-Raf!" http://127.0.0.1:8511/odunc/O-1 echo "--- curl in yolladigi baslik" curl -s -o /dev/null -u "raf-terminali:Kt-9f3a-Raf!" -v http://127.0.0.1:8511/odunc/O-1 2>&1 | grep -i "^> authorization" echo "--- yanlis parola" curl -s -w " %{http_code}\n" -u "raf-terminali:yanlis" http://127.0.0.1:8511/odunc/O-1 echo "--- turkce karakterli parola" curl -s -w " %{http_code}\n" -u "deniz.aral:üç-kitap-1876" http://127.0.0.1:8511/odunc/O-1 kill $SUNUCU
--- kimlik bilgisi yok
HTTP/1.1 401 Unauthorized
content-type: application/json; charset=utf-8
www-authenticate: Basic realm="odunc", charset="UTF-8"
--- curl -u ile
{"kullanici":"raf-terminali","kaynak":"/odunc/O-1"} 200
--- curl in yolladigi baslik
> Authorization: Basic cmFmLXRlcm1pbmFsaTpLdC05ZjNhLVJhZiE=
--- yanlis parola
{"hata":"kimlik dogrulanamadi"} 401
--- turkce karakterli parola
{"kullanici":"deniz.aral","kaynak":"/odunc/O-1"} 200
Türkçe karakter içeren parola geçti, çünkü hem istemci hem sunucu UTF-8 varsaydı. Bu varsayım
başlıkta yazmasaydı iki taraf farklı baytlar üretebilir ve doğru parola reddedilebilirdi;
charset parametresinin işlevi budur.
Taşınan Değerin Ne Olduğu
Başlıktaki değer bir kodlamadır, şifreleme değildir. Kodlamanın işlevi, keyfi baytları
başlıkta taşınabilecek karakterlere çevirmektir; gizlemek değildir. Bunu göstermek için
sunucuya gerek yok — yukarıdaki curl çıktısının ürettiği başlığın kendisi yeter.
// cozum.mjs — temel semanin tasidigi degerin ne olduguna bakar // Baslik degeri, curl -u "raf-terminali:Kt-9f3a-Raf!" ile uretilenin aynisidir. const BASLIK = "Basic cmFmLXRlcm1pbmFsaTpLdC05ZjNhLVJhZiE="; const govde = BASLIK.slice(6); console.log("taşınan değer :", govde); console.log("çözülmüş :", Buffer.from(govde, "base64").toString("utf8")); console.log("çözmek için gereken anahtar:", "yok"); // Ters yon: ayni degeri uretmek icin de anahtar gerekmiyor const yeniden = Buffer.from("raf-terminali:Kt-9f3a-Raf!", "utf8").toString("base64"); console.log("yeniden üretilen:", yeniden, "| aynı mı:", yeniden === govde);
taşınan değer : cmFmLXRlcm1pbmFsaTpLdC05ZjNhLVJhZiE= çözülmüş : raf-terminali:Kt-9f3a-Raf! çözmek için gereken anahtar: yok yeniden üretilen: cmFmLXRlcm1pbmFsaTpLdC05ZjNhLVJhZiE= | aynı mı: true
Bu çıktının tek bir sonucu vardır: parolanın gizliliği tümüyle taşıma katmanına bırakılmıştır. İnternet Nasıl Çalışır kursundaki HTTPS’in Rolü dersinde kurulan gizlilik ve bütünlük güvenceleri olmadan, temel şema parolayı ağa okunur biçimde koyar. Şifresiz bir bağlantı üzerinde bu şemayı açan bir yapılandırma reddedilmelidir; şemanın kendisinde bunu telafi edecek hiçbir parça yoktur.
Aynı sonucun ikinci bir yüzü vardır: parola, sunucuya kadar giden yol boyunca her ara bileşende okunabilir biçimdedir. Ters vekil sunucu, erişim günlüğü, hata izleme kaydı, tarayıcı geliştirme aracı — hepsi bu başlığı görür. Erişim günlüğüne bütün başlıkları yazan bir kurulum, parolayı diske de yazmış olur.
// gunluk.mjs — istek gunlugunun kimlik bilgisini nasil sizdirdigi ve nasil kapatildigi // Kullanim: node gunluk.mjs <port> (istekler stdout'a gunluk satiri yazar) import { createServer } from "node:http"; const PORT = Number(process.argv[2] ?? 8512); const GIZLENECEK = new Set(["authorization", "cookie", "proxy-authorization"]); const hamSatir = (istek) => JSON.stringify({ yol: istek.url, baslik: istek.headers }); const gizliSatir = (istek) => { const baslik = Object.fromEntries( Object.entries(istek.headers).map(([ad, deger]) => [ad, GIZLENECEK.has(ad) ? "<gizlendi>" : deger]), ); return JSON.stringify({ yol: istek.url, baslik }); }; createServer((istek, yanit) => { yanit.sendDate = false; console.log("ham :", hamSatir(istek)); console.log("gizli :", gizliSatir(istek)); yanit.writeHead(200, { "content-type": "application/json; charset=utf-8" }); yanit.end('{"tamam":true}'); }).listen(PORT, "127.0.0.1");
node gunluk.mjs 8512 & SUNUCU=$! sleep 1 curl -s -o /dev/null -H "user-agent: raf-terminali/1" -u "raf-terminali:Kt-9f3a-Raf!" http://127.0.0.1:8512/odunc/O-1 sleep 0.3 kill $SUNUCU
ham : {"yol":"/odunc/O-1","baslik":{"host":"127.0.0.1:8512","authorization":"Basic cmFmLXRlcm1pbmFsaTpLdC05ZjNhLVJhZiE=","accept":"*/*","user-agent":"raf-terminali/1"}}
gizli : {"yol":"/odunc/O-1","baslik":{"host":"127.0.0.1:8512","authorization":"<gizlendi>","accept":"*/*","user-agent":"raf-terminali/1"}}
Gizleme listesi başlık adıyla çalışır ve authorization dışında cookie ile
proxy-authorization başlıklarını da kapsar; üçü de kimlik bilgisi taşır. Bu liste,
günlükleme kurulumunun kendisinde bulunmalıdır — her günlük çağrısında elle uygulanan bir
kural, ilk unutulan çağrıda delinir.
Karşılaştırmanın Sabit Zamanlı Olması
Sunucudaki esitMi işlevi parolayı === ile karşılaştırmaz. Dizge karşılaştırması ilk
farklı karakterde durur; bu, karşılaştırmanın süresini doğru önek uzunluğuna bağlar. Süre
ölçülebilir bir kanaldır ve doğru öneki adım adım ortaya çıkarabilir.
timingSafeEqual bütün baytları her durumda karşılaştırır ve süreyi girdinin içeriğinden
bağımsız kılar. İşlev eşit uzunluk ister; bu yüzden uzunluk denetimi ayrı yapılır ve
uzunluk farkı zaten gizli bir bilgi sayılmaz. Kural, kimlik bilgisiyle ilgili her
karşılaştırma için geçerlidir: parola, oturum kimliği, imza, API anahtarı, tek kullanımlık
kod. Frontend Kalitesi kursundaki istek belirteci karşılaştırması da aynı kuralı uygular.
Kimlik Bilgisinin Her İstekte Geçmesi
Temel şemanın en belirleyici özelliği, sunucunun istekler arasında hiçbir şey
hatırlamamasıdır. Kimlik doğrulama her istekte sıfırdan yapılır, dolayısıyla parola her
istekte yeniden yollanır. Sunucudaki parolaGorulmesi sayacı bunu ölçer.
node temel.mjs 8511 > /dev/null & SUNUCU=$! sleep 1 for yol in /odunc/O-1 /odunc/O-2 /kitaplar/978-0262033848 /uyeler/U-1001; do curl -s -o /dev/null -u "raf-terminali:Kt-9f3a-Raf!" "http://127.0.0.1:8511$yol" done curl -s -u "raf-terminali:Kt-9f3a-Raf!" http://127.0.0.1:8511/sayac echo kill $SUNUCU
{"istek":5,"parolaGorulmesi":5}
Beş istek, parolanın beş kez ağa çıkması demektir. Bunun üç sonucu vardır.
Birincisi, açığa çıkma yüzeyi istek sayısıyla büyür. Tek bir kayıt, tek bir hatalı yapılandırma, tek bir yanlış hedefe giden istek parolanın kendisini verir — oysa sonraki derslerdeki şemalarda sızan şey, ömrü sınırlı bir türev olur.
İkincisi, çıkış diye bir işlem yoktur. Sunucuda silinecek bir kayıt bulunmadığı için “bu istemcinin erişimini şimdi kes” isteğinin karşılığı yoktur; tek yol parolayı değiştirmektir. Parola değişince o parolayı kullanan bütün istemciler aynı anda düşer.
Üçüncüsü, şema hesap ile istemciyi ayırmaz. Aynı hesabı üç raf terminali kullanıyorsa sunucunun gördüğü tek bir kullanıcı adıdır; hangi terminalin ne yaptığı, hangisinin erişiminin kesileceği ayırt edilemez. Ayrım isteniyorsa her istemciye ayrı hesap açmak gerekir ve bu, hesap sayısını istemci sayısına bağlar.
Şemanın Yeterli Olduğu Yer
Bu sınırlar şemayı kullanılamaz yapmaz; kullanım alanını daraltır. Temel şema, aşağıdaki koşulların hepsi sağlandığında yeterlidir:
- Bağlantı TLS ile korunuyordur ve şifresiz bağlantıda uç hiç açılmaz.
- Kimlik bilgisi bir insanın parolası değil, tek bir istemciye ayrılmış rastgele üretilmiş bir değerdir; böylece iptal, o istemciyi düşürür ve başkasını etkilemez.
- Kimlik bilgisi sunucuda doğrulayıcı biçiminde saklanır; düz metin olarak değil.
- Günlükleme ve hata izleme kurulumunda
Authorizationbaşlığı gizleme listesindedir. - İkinci etmen, oturum devamlılığı ve kullanıcıya görünen çıkış gibi gereksinimler yoktur.
Son koşul çoğu kullanıcıya dönük uygulamada sağlanmaz. Bir üye tarayıcıdan giriş yapıyorsa oturum devamlılığı, çıkış düğmesi ve ikinci etmen beklenir; temel şema bu üçünü de vermez. Buna karşılık iki servisin birbirini çağırdığı, TLS’in zorunlu olduğu ve kimlik bilgisinin makineye ait olduğu bir kurulumda şema hem yeterli hem de az parçalı olduğu için tercih edilebilir.
Özet
- Temel şema iki adımdır: sunucunun 401 ile yolladığı davet ve istemcinin
Authorizationbaşlığıyla verdiği yanıt;realmbölgeyi,charsetkodlamayı bildirir. - Başlıkta taşınan değer base64 kodlamasıdır; çözmek için anahtar gerekmez, dolayısıyla gizlilik tümüyle taşıma katmanına bağlıdır.
- Kimlik bilgisi taşıyan başlıklar günlüğe olduğu gibi yazılırsa parola diske de yazılmış olur; gizleme listesi günlükleme kurulumunun parçası olmalıdır.
- Kimlik bilgisi karşılaştırması sabit zamanlı yapılır; erken çıkan karşılaştırma doğru önek uzunluğunu süreye yansıtır.
- Parola her istekte yeniden geçer; bunun sonucu büyüyen açığa çıkma yüzeyi, çıkış işleminin bulunmaması ve hesap ile istemcinin ayrılamamasıdır.
- Şema, TLS zorunluluğu ve istemciye özel rastgele kimlik bilgisiyle birlikte makineler arası çağrılarda yeterlidir; oturum, çıkış ve ikinci etmen gerektiren akışlarda yetersizdir.
Sonraki Adım
Temel şemanın çözmediği sorunların ortak kaynağı, sunucunun istekler arasında hiçbir şey hatırlamamasıdır. Sunucu bir kez doğrulayıp sonucu kendi tarafında saklarsa parola bir daha ağa çıkmaz, çıkış işlemi anlam kazanır ve aynı hesabın farklı istemcileri ayrı ayrı izlenebilir. Sonraki ders bu kaydı kurar: girişte sunucu tarafında bir oturum açılır, istemciye yalnızca o kayda işaret eden bir kimlik verilir ve bu kimlik çerezle taşınır. Çerezin hangi niteliklerle yollandığı ve oturum kimliğinin girişte niçin yenilendiği, kurulumun doğru olup olmadığını belirleyen iki ayrıntıdır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.