İçeriğe geç
academia.sh

Ders 04 / 23

Belirteç Tabanlı Kimlik Doğrulama

Asılın isteğin taşıdığı imzalı değerin içine yazılması ve doğrulamanın yerel bir hesaba inmesi: donuk ile kendi kendine yeten belirteç ayrımı, taşıyıcı şeması, imzanın neyi güvenceye aldığı ve çıkışın anında etkili olmamasının ölçülmesi.

İçindekiler

Oturum deposu bir bedel taşıyordu: her istek deponun okunmasını gerektiriyor, uygulama yatay ölçeklendiğinde depo bütün süreçlerin ulaşabildiği bir yerde durmak zorunda kalıyordu. Bedelin kaynağı, isteğin taşıdığı değerin anlamsız olmasıydı; kimliği anlama işi hep sunucudaydı.

Bu ders bunun tersini kurar. Asılın kim olduğu ve neye yetkili olduğu, isteğin taşıdığı değerin içine yazılır; değerin değiştirilmediğinden emin olmak için de imzalanır. Sunucu doğrulama sırasında hiçbir kayda bakmaz, yalnız imzayı hesaplar. Kazanç ölçülebilir olduğu gibi kayıp da ölçülebilir: kayıt yoksa çıkış neyi siler?

Belirtecin İki Türü

Donuk belirteç (opaque token), taşıdığı değerin kendisi hiçbir anlam içermeyen bir dizgedir. Önceki dersteki oturum kimliği bu türdendir: değer bir kayda işaret eder ve o kayıt olmadan hiçbir şey söylemez. Belirteci alan bir servis, onu üreten servise sorarak — buna belirteç incelemesi (token introspection) denir — geçerli olup olmadığını ve arkasındaki asılı öğrenir.

Kendi kendine yeten belirteç (self-contained token), asıl hakkındaki bilgiyi kendi içinde taşır ve bir imzayla korunur. Doğrulayan taraf imzayı hesaplayabiliyorsa hiçbir yere sormaz. Bu türün kazancı doğrulamanın yerel olmasıdır: ödünç servisi, kataloğu ve ceza servisi aynı belirteci ayrı ayrı doğrulayabilir, aralarında paylaşılan bir oturum deposu gerekmez.

Ayrım bir ödünleşimdir. Donuk belirteçte geçerlilik kararı üretenin elindedir ve her an değiştirilebilir; karşılığında her doğrulama bir sorgu ister. Kendi kendine yeten belirteçte doğrulama ucuzdur; karşılığında karar, belirteç üretildiği anda dondurulmuştur.

İmzalı Belirtecin Üretimi ve Doğrulanması

Aşağıdaki sunucu, gövdeyi base64url ile kodlayıp HMAC-SHA256 imzasını yanına ekler. Bu biçim bu derse özgüdür; sonraki ders aynı işi yapan standart biçimi kurar.

// belirtec.mjs — kendi kendine yeten, imzali belirtecle durumsuz kimlik dogrulama
// Kullanim: node belirtec.mjs <port> [iptal]
//   iptal: cikista belirtec kimligi iptal listesine yazilir
// Ortam: OMUR_MS (belirtec omru, ontanimli 6000)
import { createServer } from "node:http";
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";

const PORT = Number(process.argv[2] ?? 8531);
const IPTAL_LISTESI_VAR = process.argv.includes("iptal");
const OMUR_MS = Number(process.env.OMUR_MS ?? 6000);

const ANAHTAR = randomBytes(32);                    // imzalama anahtari; sunucuda kalir
const HESAPLAR = new Map([["deniz.aral", { parola: "üç-kitap-1876", kod: "U-1001", rol: "uye" }]]);

// Iptal listesi: belirtec kimligi -> ne zamana kadar tutulacagi
const IPTAL = new Map();
let dogrulamaSayisi = 0, depoOkumaSayisi = 0;

const b64 = (v) => Buffer.from(v).toString("base64url");
const imzala = (govde) => createHmac("sha256", ANAHTAR).update(govde).digest("base64url");

const uret = (asil) => {
  const govde = b64(JSON.stringify({ ...asil, jti: randomBytes(9).toString("base64url"), exp: Date.now() + OMUR_MS }));
  return `${govde}.${imzala(govde)}`;
};

const dogrula = (belirtec) => {
  dogrulamaSayisi++;
  const [govde, imza] = (belirtec ?? "").split(".");
  if (!govde || !imza) return { hata: "bicim" };

  const beklenen = Buffer.from(imzala(govde), "utf8");
  const gelen = Buffer.from(imza, "utf8");
  if (beklenen.length !== gelen.length || !timingSafeEqual(beklenen, gelen)) return { hata: "imza" };

  const icerik = JSON.parse(Buffer.from(govde, "base64url").toString("utf8"));
  if (Date.now() >= icerik.exp) return { hata: "sure" };

  if (IPTAL_LISTESI_VAR) {
    depoOkumaSayisi++;                              // durumsuzluktan vazgecilen tek nokta
    if (IPTAL.has(icerik.jti)) return { hata: "iptal" };
  }
  return { icerik };
};

const govdeOku = (istek) =>
  new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); });

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const yol = istek.url.split("?")[0];
  const json = (kod, govde, baslik = {}) => {
    yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8", ...baslik });
    yanit.end(JSON.stringify(govde));
  };

  if (istek.method === "POST" && yol === "/giris") {
    const { kullanici, parola } = JSON.parse((await govdeOku(istek)) || "{}");
    const hesap = HESAPLAR.get(kullanici ?? "");
    if (!hesap || parola !== hesap.parola) return json(401, { hata: "kimlik dogrulanamadi" });
    return json(200, { belirtec: uret({ kod: hesap.kod, rol: hesap.rol }) });
  }

  const tasiyici = (istek.headers.authorization ?? "").startsWith("Bearer ")
    ? istek.headers.authorization.slice(7) : null;

  if (istek.method === "POST" && yol === "/cikis") {
    const sonuc = dogrula(tasiyici);
    if (sonuc.icerik && IPTAL_LISTESI_VAR) IPTAL.set(sonuc.icerik.jti, sonuc.icerik.exp);
    return json(200, { cikildi: true, iptalListesiVar: IPTAL_LISTESI_VAR });
  }

  if (yol === "/ben") {
    if (!tasiyici) {
      return json(401, { hata: "belirtec yok" }, { "www-authenticate": 'Bearer realm="odunc"' });
    }
    const sonuc = dogrula(tasiyici);
    if (sonuc.hata) {
      return json(401, { hata: sonuc.hata },
        { "www-authenticate": `Bearer realm="odunc", error="invalid_token", error_description="${sonuc.hata}"` });
    }
    return json(200, { kod: sonuc.icerik.kod, rol: sonuc.icerik.rol });
  }

  if (yol === "/tanilama") {
    // Suresi gecen kayitlar listeden dusulur: liste butun belirtecleri degil,
    // yalnizca omru dolmamis olanlari tutar.
    for (const [jti, exp] of IPTAL) if (Date.now() >= exp) IPTAL.delete(jti);
    return json(200, { dogrulamaSayisi, depoOkumaSayisi, iptalListesiUzunlugu: IPTAL.size });
  }
  json(404, { hata: "kaynak yok" });
}).listen(PORT, "127.0.0.1", () => console.log(`belirtec 127.0.0.1:${PORT} iptal=${IPTAL_LISTESI_VAR}`));

Belirteç, Authorization başlığında taşıyıcı şemasıyla (Bearer scheme) yollanır. Şemanın adı anlamlıdır: belirteci taşıyan herkes onun sahibidir. Başlığı ele geçiren tarafın ayrıca bir parolaya ihtiyacı yoktur; bu, temel şemadaki durumla aynı taşıma gereksinimini doğurur ve şifresiz bağlantıyı yine dışarıda bırakır.

Reddetme yanıtı, sorunun ne olduğunu WWW-Authenticate başlığında bildirir.

OMUR_MS=60000 node belirtec.mjs 8533 > /dev/null & SUNUCU=$!
sleep 1
curl -s -D - -o /dev/null -H "Authorization: Bearer bozuk.deger" http://127.0.0.1:8533/ben | grep -i "^www-authenticate"
kill $SUNUCU
www-authenticate: Bearer realm="odunc", error="invalid_token", error_description="imza"

Gövdenin Okunabilirliği

İmza, gövdenin sunucudan çıktığı hâlde olduğunu güvenceye alır. Gövdeyi gizlemez. Kodlama base64url’dir ve önceki derste görüldüğü gibi çözmek için anahtar gerekmez.

// icerik.mjs — belirtecin tasidigi govdenin okunabilirligi ve imzanin ne dogruladigi
// Kullanim: OMUR_MS=60000 node belirtec.mjs 8533 &  calisirken  node icerik.mjs 8533
const PORT = Number(process.argv[2] ?? 8533);
const TABAN = `http://127.0.0.1:${PORT}`;

const giris = await fetch(`${TABAN}/giris`, {
  method: "POST", headers: { "content-type": "application/json" },
  body: JSON.stringify({ kullanici: "deniz.aral", parola: "üç-kitap-1876" }),
});
const { belirtec } = await giris.json();
const [govde, imza] = belirtec.split(".");

console.log("gövde (base64url):", govde);
console.log("imza  (base64url):", imza);
console.log("gövdenin çözülmüş hâli:", Buffer.from(govde, "base64url").toString("utf8"));

const sor = async (deger) =>
  (await fetch(`${TABAN}/ben`, { headers: { authorization: `Bearer ${deger}` } })).status;

// Govde degistirilir, imza oldugu gibi birakilir: rolu "personel" yapan bir govde
const degisik = Buffer.from(JSON.stringify({ kod: "U-1001", rol: "personel", jti: "x", exp: Date.now() + 60000 }))
  .toString("base64url");

console.log("olduğu gibi           :", await sor(belirtec));
console.log("gövdesi değiştirilmiş :", await sor(`${degisik}.${imza}`));
console.log("imzası düşürülmüş     :", await sor(govde));
OMUR_MS=60000 node belirtec.mjs 8533 > /dev/null & SUNUCU=$!
sleep 1
node icerik.mjs 8533
kill $SUNUCU
gövde (base64url): eyJrb2QiOiJVLTEwMDEiLCJyb2wiOiJ1eWUiLCJqdGkiOiJ2V0JleEFXcjNPbkQiLCJleHAiOjE3ODUyNzIxODIzNzJ9
imza  (base64url): rKXePLI8VZZtAaq1LXbveTx7Q-ncpUX7e4EgiZHlBSo
gövdenin çözülmüş hâli: {"kod":"U-1001","rol":"uye","jti":"vWBexAWr3OnD","exp":1785272182372}
olduğu gibi           : 200
gövdesi değiştirilmiş : 401
imzası düşürülmüş     : 401

Belirteç kimliği ve zaman damgası her çalıştırmada değişir. Üç satırın söylediği ayrı ayrı okunmalıdır. Gövde okunabilir: içindeki her alan, belirteci eline geçiren herkese açıktır. Bu yüzden gövdeye parola, kimlik numarası, dâhilî tablo anahtarı ya da başka bir sır yazılmaz. Gövdenin değiştirilmesi ise sonuç vermez: rolü personel yapan bir gövdenin eski imzayla eşleşme olasılığı yoktur ve sunucu 401 döner. İmzanın düşürülmesi de aynı kapıya çıkar — imzasız gövde biçim denetiminden geçemez.

Kısacası imza bütünlüğü sağlar, gizliliği sağlamaz. İkisinin karıştırılması, gövdeye sır yazan kurulumların kaynağıdır.

İptal Sorunu

Doğrulama hiçbir kayda bakmadığı için, sunucunun “bu belirteç artık geçersiz” diyebileceği bir yer yoktur. Çıkış isteği belirteci istemciden silebilir ama başka bir kopyası varsa o kopya çalışmayı sürdürür. Süre ölçülebilir: belirteç ömrü 6 saniyeye çekilir, çıkıştan sonra /ben sürekli sorulur ve ilk başarısızlığa kadar geçen zaman yazılır.

// iptal-olcumu.mjs — cikistan sonra belirtecin kac saniye gecerli kaldigini olcer
// Kullanim: OMUR_MS=6000 node belirtec.mjs 8531 &        (iptal listesi yok)
//           OMUR_MS=6000 node belirtec.mjs 8532 iptal &  (iptal listesi var)
//           node iptal-olcumu.mjs 8531   /   node iptal-olcumu.mjs 8532
const PORT = Number(process.argv[2]);
const TABAN = `http://127.0.0.1:${PORT}`;

const giris = await fetch(`${TABAN}/giris`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ kullanici: "deniz.aral", parola: "üç-kitap-1876" }),
});
const { belirtec } = await giris.json();
const bas = { authorization: `Bearer ${belirtec}` };
const durum = async () => (await fetch(`${TABAN}/ben`, { headers: bas })).status;

console.log("çıkış öncesi /ben :", await durum());
await fetch(`${TABAN}/cikis`, { method: "POST", headers: bas });

const cikisAni = Date.now();
let gecerliKaldi = 0;
for (;;) {
  const kod = await durum();
  if (kod !== 200) break;
  gecerliKaldi = Date.now() - cikisAni;
  await new Promise((c) => setTimeout(c, 250));
}
console.log("çıkıştan sonra geçerli kaldığı süre:", (gecerliKaldi / 1000).toFixed(1), "sn");
console.log("tanılama:", await (await fetch(`${TABAN}/tanilama`)).json());
OMUR_MS=6000 node belirtec.mjs 8531 > /dev/null & LISTESIZ=$!
OMUR_MS=6000 node belirtec.mjs 8532 iptal > /dev/null & LISTELI=$!
sleep 1
echo "— iptal listesi yok —"
node iptal-olcumu.mjs 8531
echo "— iptal listesi var —"
node iptal-olcumu.mjs 8532
kill $LISTESIZ $LISTELI
— iptal listesi yok —
çıkış öncesi /ben : 200
çıkıştan sonra geçerli kaldığı süre: 5.9 sn
tanılama: { dogrulamaSayisi: 27, depoOkumaSayisi: 0, iptalListesiUzunlugu: 0 }
— iptal listesi var —
çıkış öncesi /ben : 200
çıkıştan sonra geçerli kaldığı süre: 0.0 sn
tanılama: { dogrulamaSayisi: 3, depoOkumaSayisi: 3, iptalListesiUzunlugu: 1 }

Ölçülen süreler yüzde birkaç oynar; belirleyici olan büyüklük mertebesidir. Listesiz kurulumda çıkış isteğinin sunucu tarafındaki karşılığı boştur: belirteç, ömrünün sonuna kadar çalışır. Altı saniyelik ömürde bu altı saniyedir; sekiz saatlik ömürde sekiz saattir. Önceki dersin oturum deposunda aynı ölçüm sıfır veriyordu, çünkü karar bir kaydın silinmesiydi.

Aynı çıktı, kazancın nereden geldiğini de gösteriyor. Listesiz kurulumda depoOkumaSayisi sıfırdır: yirmi yedi doğrulama yapıldı ve hiçbiri hiçbir yere sormadı. Listeli kurulumda iptalListesiUzunlugu bire çıkmıştır; çıkışta yazılan tek kayıt, ömrü dolana kadar orada durur.

İptal Listesinin Bedeli

İkinci kurulum, çıkışta belirteç kimliğini bir iptal listesine yazar ve her doğrulamada o listeye bakar. Ölçüm sıfır saniye verir: karar anında etkilidir. Ama depoOkumaSayisi artık doğrulama sayısına eşittir — durumsuzluk bu noktada bırakılmıştır.

Yine de iki kurulum arasında nicel bir fark kalır. Oturum deposu açık olan bütün oturumları tutar ve kayıt kullanıcı çıkana ya da ömür dolana kadar durur. İptal listesi yalnızca iptal edilmiş ve henüz ömrü dolmamış belirteçleri tutar; ömrü dolan kayıt listeden düşer, çünkü zaten süre denetimi onu reddedecektir. Belirteç ömrü kısaldıkça bu liste küçülür.

Buradan pratik kural çıkar: durumsuz belirteçte iptalin çözümü listeyi büyütmek değil, ömrü kısaltmaktır. Ömür kısaldığında iptal penceresi de kısalır. Ama kısa ömür, kullanıcıyı dakikada bir yeniden giriş yapmaya zorlar; bu ikilemin çözümü ayrı bir derstir.

Özet

  • Donuk belirteç bir kayda işaret eder ve doğrulaması sorgu ister; kendi kendine yeten belirteç bilgiyi kendi içinde taşır ve doğrulaması yereldir.
  • Kendi kendine yeten belirteç imzalanır; imza gövdenin değişmediğini güvenceye alır ama gövdeyi gizlemez, bu yüzden gövdeye sır yazılmaz.
  • Belirteç taşıyıcı şemasıyla yollanır; taşıyanın sahip sayılması, şifresiz bağlantıyı yine dışarıda bırakır ve reddetme nedeni WWW-Authenticate başlığında bildirilir.
  • Sunucuda kayıt olmadığı için çıkış anında etkili olmaz: ölçümde belirteç, ömrü bitene kadar çalışmayı sürdürdü.
  • İptal listesi kararı anında etkili kılar ama her doğrulamayı bir okumaya bağlar; liste yalnız ömrü dolmamış iptalleri tuttuğu için oturum deposundan küçüktür.
  • Durumsuz kurulumda iptal penceresini belirleyen değişken belirteç ömrüdür; kısa ömür pencereyi daraltır ve asıl çözüm buradadır.

Sonraki Adım

Bu derste kullanılan belirteç biçimi bu derse özgüydü: gövde, nokta, imza. Aynı işi yapan yaygın bir standart vardır ve alanların adlarını, imza algoritmasının nasıl bildirileceğini, sürenin hangi birimle yazılacağını sabitler. Standart olmak, doğrulamanın da standartlaşması demektir — ve doğrulayıcının hangi adımları atlamaması gerektiği, biçimin kendisinden daha önemlidir. Sonraki ders bu biçimi ayrıntısıyla kurar, imzayı elle üretip doğrular ve doğrulayıcının belirtecin kendi söylediğine güvendiği noktada niçin çöktüğünü gösterir.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat