---
title: 'Kimlik Doğrulama ve Yetkilendirme'
source: 'https://academia.sh/tr/kurslar/kimlik-ve-yetki'
language: tr
updated: '2026-08-17T04:38:47+00:00'
license: 'CC BY-SA 4.0'
lessons: 23
outcomes:
    - 'Kimlik doğrulama ile yetkilendirmeyi ayırır ve uygun akışı seçer'
    - 'Parola ve belirteç saklamayı güvenli biçimde gerçekler'
    - 'Yetki modelini rol, öznitelik veya ilişki temelinde kurar'
---

# Kimlik Doğrulama ve Yetkilendirme

İsteğin arkasındaki kimliğin belirlenmesi ve o kimliğin ne yapabileceğine karar verilmesi: oturum ve belirteç akışları, imza doğrulama, yenileme ve dönüşüm, yetki devri protokolleri, kimlik bilgisinin saklanması ve rol, öznitelik ya da ilişki temelli yetki modelleri.

## Kimlik Doğrulama Akışları

İsteğin sahibinin kim olduğunun sunucuda belirlenmesi: temel şemadan oturum çerezine, imzalı belirteçten yenileme dönüşümüne, yetki devri ve federasyon protokollerinden makine anahtarlarına ve ikinci etmene kadar akışların kurulumu, yanlış kurulumun gözlenebilir sonucu ve seçim ölçütleri.

### Kimlik Doğrulama ve Yetkilendirme Ayrımı

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/kimlik-dogrulama-ve-yetkilendirme-ayrimi

İsteğin arkasındaki asılın belirlenmesi ile o asılın ne yapabileceğine karar verilmesinin iki ayrı soru olduğunun gösterilmesi: yalnız birincisini soran sunucunun gözlenebilir sonucu, 401 ile 403 ayrımı ve kararın öntanımlı ret üzerine kurulması.

Önceki kurs boyunca ödünç servisinin sözleşmesi kuruldu: hangi adres hangi kaynağı verir,
hangi yöntem neyi değiştirir, hata hangi biçimde bildirilir, sözleşme nasıl belgelenir ve
sınanır. Bütün bu kararlar tek bir soruyu hiç sormadı: bu isteği **kim** yolluyor? Sahte
sunucu her isteğe aynı yanıtı verdi, sözleşme sınaması her etkileşimi aynı ayrıcalıkla
çalıştırdı. Sözleşme, çağıranın kim olduğundan bağımsız yazılmıştı.

Gerçek serviste bu bağımsızlık sürmez. Ödünç kaydını üye kendisi için okuyabilir, gecikme
raporunu yalnız personel görebilir, bir üyenin cezasını yalnız şube yöneticisi silebilir. Bu
cümlelerin her biri iki ayrı bilgi içerir: isteğin arkasındaki kimliğin ne olduğu ve o
kimliğin ne yapmaya izinli olduğu. Bu kurs bu iki bilgiyi ayırmakla başlar, çünkü ikisini
tek soru sanan bir sunucu her zaman aynı biçimde bozulur.

#### İki Ayrı Soru

**Kimlik doğrulama** (authentication) "sen kimsin?" sorusunu yanıtlar. Sunucu, isteğin
taşıdığı **kimlik bilgisinden** (credential) yola çıkarak isteğin arkasındaki **asılı**
(principal) belirler. Asıl bir kişi olabilir, bir servis hesabı olabilir, bir raf terminali
olabilir. Bu adımın çıktısı tek bir şeydir: bir kimlik ya da hiç.

**Yetkilendirme** (authorization) "bunu yapabilir misin?" sorusunu yanıtlar. Girdisi asıl,
istenen eylem ve eylemin hedefidir; çıktısı bir karardır: izin ver ya da reddet. Bu adım
kimliği kendisi bulmaz, birinci adımın bulduğunu kullanır.

Sıra tersine çevrilemez. Kimliği bilmeden yetki kararı verilemez, çünkü kararın girdisi
eksiktir. Ama tersi sık görülür: kimlik doğru belirlenir, sonra yetki sorusu hiç sorulmaz.
Sunucu asılın var olmasını yeterli sayar ve "kayıtlı olan her şeyi yapabilir" kuralını
farkında olmadan uygular. Bu kuralın sonucu kodda görünmez; ancak istekler çalıştırılınca
görünür.

#### Yalnız Birinci Sorunun Sorulduğu Sunucu

Aşağıdaki sunucu üç uç sunar ve kimliği doğru belirler: tanınmayan bir kimlik bilgisi 401
alır. Yetki denetimi ise bir bayrağın arkasındadır; bayrak verilmediğinde ikinci soru hiç
sorulmaz.

```js
// odunc.mjs — odunc servisi; kimlik dogrulama var, yetkilendirme secmeli
// Kullanim: node odunc.mjs <port> [yetki]
//   yetki: verilirse rol denetimi de calisir
import { createServer } from "node:http";

const PORT = Number(process.argv[2] ?? 8501);
const YETKI = process.argv[3] === "yetki";

// Kimlik bilgisi -> asil (principal). Bu esleme sonraki derslerin konusu;
// burada kimligin belirlenmis oldugu varsayiliyor.
const KIMLIKLER = new Map([
  ["k-uye-1001", { kod: "U-1001", ad: "Deniz Aral", rol: "uye" }],
  ["k-personel-7", { kod: "P-0007", ad: "Selin Kaya", rol: "personel" }],
  ["k-yonetici-2", { kod: "Y-0002", ad: "Ahmet Ünal", rol: "sube-yoneticisi" }],
]);

const ODUNC = new Map([
  ["O-1", { id: "O-1", uye: "U-1001", isbn: "978-0262033848", sonTarih: "2026-03-20", durum: "acik" }],
  ["O-2", { id: "O-2", uye: "U-1042", isbn: "978-0201896831", sonTarih: "2026-02-11", durum: "gecikmis" }],
]);

// Uc -> o ucu cagirabilecek roller
const IZIN = new Map([
  ["GET /odunc", ["uye", "personel", "sube-yoneticisi"]],
  ["GET /raporlar/gecikmeler", ["personel", "sube-yoneticisi"]],
  ["POST /cezalar/silme", ["sube-yoneticisi"]],
]);

createServer((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));
  };

  // 1. soru: kim? — kimlik bilgisi yoksa ya da taninmiyorsa 401
  const bilgi = (istek.headers.authorization ?? "").replace(/^Kimlik /, "");
  const asil = KIMLIKLER.get(bilgi);
  if (!asil) {
    return json(401, { hata: "kimlik dogrulanamadi" }, { "www-authenticate": 'Kimlik realm="odunc"' });
  }

  // Ucun adi
  const uc = yol.startsWith("/odunc/") ? "GET /odunc"
    : yol === "/raporlar/gecikmeler" ? "GET /raporlar/gecikmeler"
    : yol === "/cezalar/silme" ? "POST /cezalar/silme"
    : null;
  if (!uc || !uc.startsWith(istek.method)) return json(404, { hata: "kaynak yok" });

  // 2. soru: ne yapabilir? — yalnizca YETKI acikken soruluyor
  if (YETKI && !IZIN.get(uc).includes(asil.rol)) {
    return json(403, { hata: "bu islem icin yetkiniz yok", rol: asil.rol, gerekli: IZIN.get(uc) });
  }

  if (uc === "GET /odunc") {
    const kayit = ODUNC.get(yol.slice("/odunc/".length));
    return kayit ? json(200, kayit) : json(404, { hata: "kayit yok" });
  }
  if (uc === "GET /raporlar/gecikmeler") {
    return json(200, { gecikmis: [...ODUNC.values()].filter((k) => k.durum === "gecikmis") });
  }
  return json(200, { silindi: true, isleyen: asil.kod });
}).listen(PORT, "127.0.0.1", () => console.log(`odunc 127.0.0.1:${PORT} yetki=${YETKI}`));
```

Karar tablosunu okumak için her rolü her uçta deneyen küçük bir istemci yeter.

```js
// dizge.mjs — her rolun her uctan aldigi durum kodunu tablolar
// Kullanim: node odunc.mjs 8501 &  ve  node odunc.mjs 8502 yetki &  calisirken
//           node dizge.mjs 8501    /    node dizge.mjs 8502
const PORT = Number(process.argv[2] ?? 8501);
const TABAN = `http://127.0.0.1:${PORT}`;

const ROLLER = [["uye", "k-uye-1001"], ["personel", "k-personel-7"], ["yönetici", "k-yonetici-2"], ["tanımsız", "k-yok"]];
const UCLAR = [["GET", "/odunc/O-1"], ["GET", "/raporlar/gecikmeler"], ["POST", "/cezalar/silme"]];

console.log("rol".padEnd(10) + UCLAR.map(([y, u]) => `${y} ${u}`.padEnd(26)).join(""));
for (const [ad, bilgi] of ROLLER) {
  const kodlar = [];
  for (const [yontem, yol] of UCLAR) {
    const cevap = await fetch(TABAN + yol, { method: yontem, headers: { authorization: `Kimlik ${bilgi}` } });
    kodlar.push(String(cevap.status).padEnd(26));
  }
  console.log(ad.padEnd(10) + kodlar.join(""));
}
```

İki sunucu aynı anda ayağa kaldırılır; biri yetki denetimsiz, diğeri denetimli.

```bash
node odunc.mjs 8501 > /dev/null & DENETIMSIZ=$!
node odunc.mjs 8502 yetki > /dev/null & DENETIMLI=$!
sleep 1
echo "— yetki denetimi yok —"
node dizge.mjs 8501
echo "— yetki denetimi var —"
node dizge.mjs 8502
kill $DENETIMSIZ $DENETIMLI
```

```
— yetki denetimi yok —
rol       GET /odunc/O-1            GET /raporlar/gecikmeler  POST /cezalar/silme
uye       200                       200                       200
personel  200                       200                       200
yönetici  200                       200                       200
tanımsız  401                       401                       401
— yetki denetimi var —
rol       GET /odunc/O-1            GET /raporlar/gecikmeler  POST /cezalar/silme
uye       200                       403                       403
personel  200                       200                       403
yönetici  200                       200                       200
tanımsız  401                       401                       401
```

İlk tabloda dört satırın üçü aynıdır. Kimlik doğrulama çalışıyor: tanınmayan kimlik bilgisi
her uçta 401 alıyor. Denetimin verdiği tek ayrım "kayıtlı" ile "kayıtsız" arasındadır. Üye
kimliğiyle gelen bir istek gecikme raporunun tamamını okuyor ve bir cezayı siliyor; yanıtın
`isleyen` alanı silme işleminin U-1001 adına yapıldığını söylüyor.

Bu, kimlik doğrulamanın bir kusuru değildir; kimlik doğrulama görevini yapmıştır. Kusur,
yetki kararının hiç verilmemiş olmasıdır. İkinci tabloda aynı istekler 403 alır ve tablo
artık üç farklı satır içerir. Aradaki tek fark, isteği işleyen kodun ikinci soruyu sormasıdır.

#### 401 ve 403 Ayrımı

İki durum kodu iki farklı soruya karşılık gelir ve birbirinin yerine kullanılamaz.

**401**, birinci sorunun yanıtsız kaldığını bildirir: kimlik bilgisi yok, biçimsiz ya da
tanınmıyor. Yanıt, kimliğin nasıl sunulacağını söyleyen `WWW-Authenticate` başlığını taşımak
zorundadır; bu başlık olmadan istemcinin ne yapacağı belirsiz kalır.

```bash
node odunc.mjs 8502 yetki > /dev/null & SUNUCU=$!
sleep 1
curl -s -D - -o /dev/null http://127.0.0.1:8502/cezalar/silme | head -3
kill $SUNUCU
```

```
HTTP/1.1 401 Unauthorized
content-type: application/json; charset=utf-8
www-authenticate: Kimlik realm="odunc"
```

**403**, birinci sorunun yanıtlandığını ama ikincisinin ret verdiğini bildirir. Kimlik
bellidir ve geçerlidir; eksik olan yetkidir. Aynı kimlik bilgisini yeniden yollamak sonucu
değiştirmez, çünkü sorun kimlik bilgisinde değildir.

```bash
node odunc.mjs 8502 yetki > /dev/null & SUNUCU=$!
sleep 1
curl -s -w ' %{http_code}\n' -X POST -H 'Authorization: Kimlik k-uye-1001' http://127.0.0.1:8502/cezalar/silme
kill $SUNUCU
```

```
{"hata":"bu islem icin yetkiniz yok","rol":"uye","gerekli":["sube-yoneticisi"]} 403
```

Bu ayrım istemcinin davranışını belirler. 401 alan istemci kimlik bilgisini tazeleyip yeniden
dener; 403 alan istemci yeniden denemez, kullanıcıya yetkisinin yetmediğini söyler. Kodları
karıştıran bir sunucu, 403 yerine 401 döndürerek istemciyi sonsuz bir tazeleme döngüsüne
sokar.

Üçüncü bir seçenek de vardır: bir kaynağın **varlığının** kendisi gizli bilgiyse, yetkisiz
isteğe 403 yerine 404 dönmek gerekebilir. 403, "böyle bir kaynak var ama sana kapalı" der ve
bu cümle bazı durumlarda söylenmemesi gereken bir bilgidir. Bu seçim bilinçli yapılmalıdır;
her yerde 404 dönmek de hata ayıklamayı sağır eder.

#### Kararın Nerede Verildiği

Yetki kararının kod içinde bir **karar noktası** vardır. Yukarıdaki sunucuda bu nokta tek bir
`if` bloğudur ve isteği işleyen koddan önce gelir. İki özelliği önemlidir.

Birincisi, karar noktası **öntanımlı ret** üzerine kurulur. `IZIN` tablosunda karşılığı
olmayan bir uç, kimseye açık değildir. Bunun tersi olan kurulumda — "tabloda yasaklanmayan
her şey serbest" — tabloya eklenmeyi unutmuş her yeni uç, yazıldığı anda herkese açılır.
Yeni uç eklemek sık bir iştir; unutmak da öyle. Öntanımlı ret, unutmanın sonucunu erişim
açığından çalışmayan uca çevirir: sonuç yine hatadır ama gözlenebilir ve zararsız bir
hatadır.

İkincisi, karar noktası **tek** olmalıdır. Aynı kararın iki yerde verildiği bir kodda ikisi
zamanla ayrışır ve hangisinin geçerli olduğu ancak isteği çalıştırarak bulunur. Ara katman
zinciri — Sunucu Tarafı Temelleri kursunda kurulan yapı — bu tekliği sağlamanın doğal yeridir:
kimliği belirleyen katman isteğe asılı iliştirir, yetki katmanı o asıla bakar, iş mantığı
ikisini de yeniden hesaplamaz.

Bu iki soru, denetimin tamamı değildir. İkinci tablodaki `uye` satırında `GET /odunc/O-1`
hücresi 200'dür ve doğrudur: üyeler ödünç kaydı okuyabilir. Ama aynı üye başkasının kaydını
da okuyabilir.

```bash
node odunc.mjs 8502 yetki > /dev/null & SUNUCU=$!
sleep 1
curl -s -w ' %{http_code}\n' -H 'Authorization: Kimlik k-uye-1001' http://127.0.0.1:8502/odunc/O-2
kill $SUNUCU
```

```
{"id":"O-2","uye":"U-1042","isbn":"978-0201896831","sonTarih":"2026-02-11","durum":"gecikmis"} 200
```

U-1001 kimliğiyle gelen istek, U-1042 üyesinin kaydını aldı. Rol denetimi geçti çünkü soru
"üyeler ödünç kaydı okuyabilir mi?" biçiminde soruldu; sorulması gereken "bu üye **bu**
kaydı okuyabilir mi?" idi. Rol, eylem türünü yetkilendirir; nesnenin sahipliğini
yetkilendirmez. Bu ayrım Yetkilendirme Modelleri konusunun kendi başına bir dersidir; burada
yalnız şu tespit yeterlidir: yetki sorusu bir uç adına değil, bir asıl–eylem–nesne üçlüsüne
sorulur.

#### Özet

- Kimlik doğrulama isteğin arkasındaki asılı belirler; yetkilendirme o asılın belirli bir
  eylemi yapıp yapamayacağına karar verir. İkincisinin girdisi birincisinin çıktısıdır.
- Yalnız kimlik doğrulayan bir sunucu "kayıtlı olan her şeyi yapabilir" kuralını uygular;
  bu kural kodda değil, erişim tablosunda görünür.
- 401 kimlik bilgisinin eksik ya da tanınmaz olduğunu bildirir ve `WWW-Authenticate`
  başlığını taşır; 403 kimliğin bilindiğini ama yetkinin yetmediğini bildirir.
- Kaynağın varlığı gizli bilgiyse yetkisiz isteğe 404 dönmek bilinçli bir seçenektir;
  varsayılan değildir.
- Karar noktası öntanımlı ret üzerine kurulur ve tektir; tabloya eklenmeyen uç kapalı kalır.
- Rol denetimi eylem türünü yetkilendirir, nesnenin sahipliğini yetkilendirmez; yetki sorusu
  asıl–eylem–nesne üçlüsüne sorulur.

#### Sonraki Adım

Bu derste kimliğin nasıl belirlendiği bir eşleme tablosunun arkasına saklandı: istek bir
dizge taşıdı, sunucu onu bir asıla çevirdi. Gerçek bir kurulumda bu dizgenin nereden geldiği,
nasıl taşındığı ve ne kadar süre geçerli olduğu ayrı kararlardır. En eski ve en küçük yanıt,
HTTP'nin kendi içinde tanımlı temel şemadır: kullanıcı adı ve parola her istekte
`Authorization` başlığında taşınır. Sonraki ders bu şemayı çalıştırarak kurar, taşınan
değerin niçin şifrelenmiş sayılmadığını gösterir ve şemanın nerede yeterli, nerede yetersiz
kaldığını sınırlarıyla belirler.

### Temel Kimlik Doğrulama

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/temel-kimlik-dogrulama

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ı.

Ö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.

```js
// 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}`));
```

```bash
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.

```js
// 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.

```js
// 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");
```

```bash
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.

```bash
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 `Authorization` baş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 `Authorization`
  başlığıyla verdiği yanıt; `realm` bölgeyi, `charset` kodlamayı 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.

### Oturum Tabanlı Kimlik Doğrulama

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/oturum-tabanli-kimlik-dogrulama

Sunucu tarafında tutulan oturum kaydının kurulması ve oturum kimliğinin çerezle taşınması: çerez niteliklerinin yanıt başlığında doğrulanması, girişte kimliğin yenilenmesinin ölçülmesi, çıkışın anında etkili olması ve boşta kalma ile mutlak ömür sınırları.

Temel şemanın çözmediği sorunların ortak kaynağı, sunucunun istekler arasında hiçbir şey
hatırlamamasıydı. Parola her istekte yeniden geçiyordu, çıkış diye bir işlem yoktu ve aynı
hesabın farklı istemcileri ayırt edilemiyordu. Sunucu bir kez doğrulayıp sonucu kendi
tarafında saklarsa üçü de değişir.

Bu derste kurulan yapı **oturumdur** (session): girişte sunucuda bir kayıt açılır, kayda
rastgele bir **oturum kimliği** (session identifier) verilir ve istemci sonraki isteklerde
yalnız bu kimliği taşır. Parola bir daha ağa çıkmaz. Kimliğin kendisi bir anlam taşımaz;
tek işlevi sunucudaki kayda işaret etmektir. Kurulumun doğru olup olmadığını belirleyen iki
ayrıntı, kimliğin hangi niteliklerle taşındığı ve girişte yenilenip yenilenmediğidir.

#### Oturum Deposu

Aşağıdaki sunucu bir oturum deposu tutar, girişte kayıt açar, çerez kurar ve her istekte
çerezdeki kimliği kayda çevirir. İki bayrak alır: `guvenli`, çerezi TLS arkasındaki bir
kuruluma göre yollar; `sabit`, girişte kimliği yenilemeyen yanlış kurulumu üretir ve dersin
karşı örneğidir.

```js
// oturum.mjs — sunucu tarafi oturum deposu ve cerezle tasinan oturum kimligi
// Kullanim: node oturum.mjs <port> [guvenli] [sabit]
//   guvenli: cerez __Host- onekiyle, Secure niteligiyle yollanir (TLS arkasi kurulum)
//   sabit  : giriste oturum kimligi YENILENMEZ (yanlis kurulum; dersin karsi ornegi)
// Ortam: BOSTA_MS (bosta kalma siniri), MUTLAK_MS (mutlak omur)
import { createServer } from "node:http";
import { randomBytes, timingSafeEqual } from "node:crypto";

const PORT = Number(process.argv[2] ?? 8521);
const GUVENLI = process.argv.includes("guvenli");
const SABIT = process.argv.includes("sabit");
const BOSTA_MS = Number(process.env.BOSTA_MS ?? 15 * 60_000);
const MUTLAK_MS = Number(process.env.MUTLAK_MS ?? 8 * 3_600_000);

const CEREZ_ADI = GUVENLI ? "__Host-odunc_oturum" : "odunc_oturum";
const HESAPLAR = new Map([["deniz.aral", { parola: "üç-kitap-1876", kod: "U-1001", rol: "uye" }]]);

// Oturum deposu: kimlik -> kayit. Tek surecte Map; birden cok surecte paylasilan bir depo.
const DEPO = new Map();
const yeniKimlik = () => randomBytes(32).toString("base64url");

const esitMi = (a, b) => {
  const x = Buffer.from(a, "utf8"), y = Buffer.from(b, "utf8");
  return x.length === y.length && timingSafeEqual(x, y);
};

const cerezOku = (baslik, ad) =>
  (baslik ?? "").split(";").map((p) => p.trim().split("="))
    .filter(([a]) => a === ad).map(([, d]) => d)[0];

const cerezYaz = (kimlik, omurSn) => {
  const parcalar = [`${CEREZ_ADI}=${kimlik}`, "Path=/", "HttpOnly", "SameSite=Lax", `Max-Age=${omurSn}`];
  if (GUVENLI) parcalar.splice(2, 0, "Secure");
  return parcalar.join("; ");
};

const oturumCoz = (istek) => {
  const kimlik = cerezOku(istek.headers.cookie, CEREZ_ADI);
  if (!kimlik) return { kimlik: null, kayit: null };
  const kayit = DEPO.get(kimlik);
  if (!kayit) return { kimlik, kayit: null };
  const simdi = Date.now();
  if (simdi - kayit.sonErisim > BOSTA_MS || simdi - kayit.acilis > MUTLAK_MS) {
    DEPO.delete(kimlik);                       // suresi gecen kayit depodan dusulur
    return { kimlik, kayit: null };
  }
  kayit.sonErisim = simdi;
  return { kimlik, kayit };
};

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 || !esitMi(parola ?? "", hesap.parola)) return json(401, { hata: "kimlik dogrulanamadi" });

    const { kimlik: oncekiKimlik } = oturumCoz(istek);
    let kimlik;
    if (SABIT && oncekiKimlik) {
      kimlik = oncekiKimlik;                   // yanlis: giris oncesi kimlik korunuyor
    } else {
      if (oncekiKimlik) DEPO.delete(oncekiKimlik);   // dogru: eski kayit silinir
      kimlik = yeniKimlik();                         // dogru: yeni kimlik uretilir
    }
    DEPO.set(kimlik, { asil: { kod: hesap.kod, rol: hesap.rol }, acilis: Date.now(), sonErisim: Date.now() });
    return json(200, { girdi: hesap.kod }, { "set-cookie": cerezYaz(kimlik, Math.floor(MUTLAK_MS / 1000)) });
  }

  if (istek.method === "POST" && yol === "/cikis") {
    const { kimlik } = oturumCoz(istek);
    if (kimlik) DEPO.delete(kimlik);           // kayit silinir: karar bir sonraki istekte etkilidir
    return json(200, { cikildi: true }, { "set-cookie": cerezYaz("", 0) });
  }

  if (yol === "/ben") {
    const { kayit } = oturumCoz(istek);
    if (!kayit) return json(401, { hata: "oturum yok" });
    return json(200, { kod: kayit.asil.kod, rol: kayit.asil.rol });
  }

  // Tanilama ucu: yalniz bu derste, depoyu gozlemek icin. Gercek serviste bulunmaz.
  if (yol === "/tanilama/depo") {
    const { kimlik } = oturumCoz(istek);
    return json(200, {
      depodakiKayitSayisi: DEPO.size,
      cerezdekiKimlikOnEki: kimlik ? kimlik.slice(0, 12) : null,
      cerezdekiKimlikDepoda: kimlik ? DEPO.has(kimlik) : null,
    });
  }
  json(404, { hata: "kaynak yok" });
}).listen(PORT, "127.0.0.1", () => console.log(`oturum 127.0.0.1:${PORT} guvenli=${GUVENLI} sabit=${SABIT}`));
```

Oturum kimliği 32 baytlık rastgele veriden üretilir. Bu değerin tahmin edilememesi, kaydın
tek koruması olduğu için pazarlık konusu değildir: sayaçtan, kullanıcı kodundan ya da zaman
damgasından türetilen bir kimlik, kaydın kendisini herkese açar.

#### Çerez Nitelikleri

Oturum kimliği çerezle taşınır ve çerezin nitelikleri kimliğin nereye, ne zaman ve kim
tarafından gönderileceğini belirler. Aynı sunucunun iki kurulumu, yanıt başlığında yan yana
görülebilir.

```bash
node oturum.mjs 8521 > /dev/null & YEREL=$!
node oturum.mjs 8522 guvenli > /dev/null & TLS=$!
sleep 1
echo "--- yerel kurulum (TLS yok): Set-Cookie"
curl -s -D - -o /dev/null -X POST -H "content-type: application/json" \
  -d "{\"kullanici\":\"deniz.aral\",\"parola\":\"üç-kitap-1876\"}" \
  http://127.0.0.1:8521/giris | grep -i "^set-cookie"
echo "--- TLS arkasi kurulum: Set-Cookie"
curl -s -D - -o /dev/null -X POST -H "content-type: application/json" \
  -d "{\"kullanici\":\"deniz.aral\",\"parola\":\"üç-kitap-1876\"}" \
  http://127.0.0.1:8522/giris | grep -i "^set-cookie"
kill $YEREL $TLS
```

```
--- yerel kurulum (TLS yok): Set-Cookie
set-cookie: odunc_oturum=ThJZuB7-8PQvlgmrsRE-F9akNm_i_Rp3KIhauu9VLpc; Path=/; HttpOnly; SameSite=Lax; Max-Age=28800
--- TLS arkasi kurulum: Set-Cookie
set-cookie: __Host-odunc_oturum=ehLOT_FJv58aS-OxyMFUTrWNGvkZNewoIirc7qnhRsc; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=28800
```

Kimlik değerleri her çalıştırmada değişir; sabit olan niteliklerdir.

`HttpOnly`, çerezi betikten okunamaz kılar. Etkisi tarayıcıda ortaya çıkar: `document.cookie`
bu çerezi görmez. Frontend Kalitesi kursundaki Siteler Arası Betik Çalıştırma dersinde
gösterilen sızıntı yolu, oturum kimliği için bu nitelikle kapatılır. Sunucu tarafında
görülebilecek tek şey niteliğin yollanmış olmasıdır; uygulanması istemcinin işidir.

`Secure`, çerezin yalnız TLS bağlantısında gönderilmesini şart koşar. Yukarıdaki iki çıktının
tek anlamlı farkı budur ve TLS arkasındaki kurulum için isteğe bağlı değildir: niteliksiz
çerez, şifresiz bir isteğe düşen istemcide açık biçimde ağa çıkar. Yerel kurulumda niteliğin
verilmemesinin nedeni, tarayıcının şifresiz bir kökenden gelen `Secure` çerezi zaten
saklamamasıdır; bu, geliştirme ortamının TLS ile çalışması için ayrıca bir gerekçedir.

`SameSite=Lax`, çerezin başka bir siteden başlatılan isteklere iliştirilmesini sınırlar.
Sunucunun ortam yetkisiyle çalışması — yani çerezin isteğe kim başlatırsa başlatsın
eklenmesi — siteler arası istek sahteciliğinin kaynağıdır; bu nitelik yüzeyi daraltır ama
tek başına yeterli savunma değildir.

`Path=/` çerezin hangi yollara gönderileceğini, `Max-Age` ise istemcide ne kadar
saklanacağını söyler. `__Host-` öneki, konak öneki kuralı gereği çerezin `Secure` olmasını,
`Path=/` taşımasını ve `Domain` belirtmemesini şart koşar; alt alan adlarından çerez
yazılmasını engeller.

Bu niteliklerin hiçbiri sunucudaki kaydın ömrünü belirlemez. `Max-Age` istemcinin çerezi ne
zaman unutacağını söyler; kaydın ne zaman geçersizleşeceğine sunucu karar verir. İkisi
karıştırıldığında ortaya "istemci sildi ama sunucuda hâlâ geçerli" durumu çıkar.

#### Girişte Kimliğin Yenilenmesi

Giriş öncesinde istemcinin üzerinde bir oturum kimliği bulunabilir. Sunucu girişte bu kimliği
korursa, giriş öncesinde bilinen bir değer giriş sonrasında yetkili bir kayda işaret etmeye
başlar. Bu duruma **oturum sabitlemesi** (session fixation) denir; değerin istemciye nasıl
yerleştiği ayrı bir konudur, sunucunun ödevi değeri devralmamaktır.

Aşağıdaki ölçüm, giriş öncesi ve sonrası kimlikleri karşılaştırır.

```js
// sabitleme.mjs — giriste oturum kimliginin yenilenip yenilenmediginin olculmesi
// Kullanim: node oturum.mjs 8523 sabit &  ve  node oturum.mjs 8524 &  calisirken
//           node sabitleme.mjs 8523   /   node sabitleme.mjs 8524
const PORT = Number(process.argv[2]);
const TABAN = `http://127.0.0.1:${PORT}`;
const CEREZ_ADI = "odunc_oturum";

// Giris oncesi istemcinin uzerinde bulunan, disaridan belirlenmis bir deger.
const ONCEDEN_BELIRLENMIS = "onceden-belirlenmis-deger-0001";

const girisYap = async (cerezDegeri) => {
  const cevap = await fetch(`${TABAN}/giris`, {
    method: "POST",
    headers: { "content-type": "application/json", cookie: `${CEREZ_ADI}=${cerezDegeri}` },
    body: JSON.stringify({ kullanici: "deniz.aral", parola: "üç-kitap-1876" }),
  });
  const kurulan = (cevap.headers.get("set-cookie") ?? "").split(";")[0].split("=").slice(1).join("=");
  return { kod: cevap.status, kurulan };
};

const benKim = async (cerezDegeri) => {
  const cevap = await fetch(`${TABAN}/ben`, { headers: { cookie: `${CEREZ_ADI}=${cerezDegeri}` } });
  return `${cevap.status} ${await cevap.text()}`;
};

const { kurulan } = await girisYap(ONCEDEN_BELIRLENMIS);
console.log("giriş öncesi değer      :", ONCEDEN_BELIRLENMIS);
console.log("giriş sonrası kurulan   :", kurulan.slice(0, 30) + (kurulan.length > 30 ? "…" : ""));
console.log("kimlik yenilendi mi     :", kurulan !== ONCEDEN_BELIRLENMIS);
console.log("eski değerle /ben       :", await benKim(ONCEDEN_BELIRLENMIS));
console.log("yeni değerle /ben       :", await benKim(kurulan));
```

```bash
node oturum.mjs 8523 sabit > /dev/null & SABIT=$!
node oturum.mjs 8524 > /dev/null & DOGRU=$!
sleep 1
echo "— giriste kimlik yenilenmiyor (yanlis kurulum) —"
node sabitleme.mjs 8523
echo "— giriste kimlik yenileniyor (dogru kurulum) —"
node sabitleme.mjs 8524
kill $SABIT $DOGRU
```

```
— giriste kimlik yenilenmiyor (yanlis kurulum) —
giriş öncesi değer      : onceden-belirlenmis-deger-0001
giriş sonrası kurulan   : onceden-belirlenmis-deger-0001
kimlik yenilendi mi     : false
eski değerle /ben       : 200 {"kod":"U-1001","rol":"uye"}
yeni değerle /ben       : 200 {"kod":"U-1001","rol":"uye"}
— giriste kimlik yenileniyor (dogru kurulum) —
giriş öncesi değer      : onceden-belirlenmis-deger-0001
giriş sonrası kurulan   : Qdk_zPWOapHWgrDAvPOPnKvzzhQA-z…
kimlik yenilendi mi     : true
eski değerle /ben       : 401 {"hata":"oturum yok"}
yeni değerle /ben       : 200 {"kod":"U-1001","rol":"uye"}
```

İlk kurulumda giriş öncesinde bilinen değer, giriş sonrasında U-1001 üyesinin kaydını
döndürüyor. İkinci kurulumda aynı değer 401 alıyor; kayıt yeni ve tahmin edilemez bir
kimliğe taşınmış, eskisi depodan silinmiştir. Kural tek cümledir: yetki düzeyinin değiştiği
her noktada — giriş, hesap değiştirme, ikinci etmen doğrulaması, yetki yükseltme — oturum
kimliği yenilenir.

#### Çıkışın Etkisi ve Ömür Sınırları

Oturum kaydı sunucuda durduğu için silme işlemi doğrudan bir sonuç üretir. Kayıt yoksa bir
sonraki istek 401 alır; arada bekleme yoktur.

Kayıt kendiliğinden de düşer. **Boşta kalma sınırı** (idle timeout), son istekten bu yana
geçen süreyi sınırlar; her istek sayacı sıfırlar. **Mutlak ömür** (absolute lifetime),
kaydın açılışından itibaren toplam süreyi sınırlar ve istekle uzamaz. Aşağıdaki ölçümde
boşta kalma sınırı 1,5 saniyeye çekilmiştir.

```js
// omur.mjs — cikisin ve bosta kalma sinirinin ne zaman etkili oldugu
// Kullanim: BOSTA_MS=1500 node oturum.mjs 8525 &  calisirken  node omur.mjs 8525
const PORT = Number(process.argv[2] ?? 8525);
const TABAN = `http://127.0.0.1:${PORT}`;
const CEREZ_ADI = "odunc_oturum";

const giris = await fetch(`${TABAN}/giris`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ kullanici: "deniz.aral", parola: "üç-kitap-1876" }),
});
const cerez = (giris.headers.get("set-cookie") ?? "").split(";")[0];
const bas = { cookie: cerez };
const durum = async (yol) => (await fetch(TABAN + yol, { headers: bas })).status;
const depo = async () => await (await fetch(`${TABAN}/tanilama/depo`, { headers: bas })).json();

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

// Ikinci oturum: bosta kalma siniri
const giris2 = await fetch(`${TABAN}/giris`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ kullanici: "deniz.aral", parola: "üç-kitap-1876" }),
});
const bas2 = { cookie: (giris2.headers.get("set-cookie") ?? "").split(";")[0] };
const durum2 = async () => (await fetch(`${TABAN}/ben`, { headers: bas2 })).status;
console.log("yeni oturum /ben        :", await durum2());
await new Promise((c) => setTimeout(c, 800));
console.log("0,8 sn sonra /ben       :", await durum2(), "(sınır 1,5 sn; her istek sayacı sıfırlar)");
await new Promise((c) => setTimeout(c, 1800));
console.log("1,8 sn boşta sonra /ben :", await durum2());
```

```bash
BOSTA_MS=1500 node oturum.mjs 8525 > /dev/null & SUNUCU=$!
sleep 1
node omur.mjs 8525
kill $SUNUCU
```

```
giriş sonrası /ben      : 200 | depo: 1
çıkış sonrası /ben      : 401 | depo: 0
yeni oturum /ben        : 200
0,8 sn sonra /ben       : 200 (sınır 1,5 sn; her istek sayacı sıfırlar)
1,8 sn boşta sonra /ben : 401
```

Çıkış isteğinden sonra depoda kayıt kalmadı ve aynı çerezle yapılan istek 401 aldı. Bu,
oturum tabanlı kurulumun ayırt edici özelliğidir ve sonraki dersin karşılaştırma noktasıdır.

#### Deponun Bedeli

Depo sunucuda durduğu için bir bedeli vardır. Her istek deponun okunmasını gerektirir; tek
süreçte bu bir `Map` erişimidir, birden çok süreçte paylaşılan bir depoya yapılan ağ
çağrısıdır. Uygulama yatay ölçeklendiğinde kayıt bütün süreçlerin ulaşabildiği bir yerde
durmalıdır; her sürecin kendi belleğinde tuttuğu depo, isteği başka sürece düşen kullanıcıyı
oturumsuz bırakır. Bu sorunu isteği hep aynı sürece yönlendirerek çözmek, süreç düştüğünde
o sürece bağlı bütün oturumları düşürür.

Buna karşılık depo, ölçülebilir bir yetenek verir: hangi hesabın kaç açık oturumu olduğu,
hangi oturumun ne zaman kullanıldığı ve hangi oturumun tek bir istekle kapatılacağı
sunucunun bildiği bilgilerdir.

#### Özet

- Oturum, girişte sunucuda açılan bir kayıttır; istemci yalnız o kayda işaret eden rastgele
  bir oturum kimliği taşır ve parola bir daha ağa çıkmaz.
- Çerez nitelikleri kimliğin nereye ve ne zaman gönderileceğini belirler: `HttpOnly` betikten
  okumayı, `Secure` şifresiz gönderimi, `SameSite` başka siteden başlatılan isteklere
  iliştirilmesini sınırlar; `__Host-` öneki bu kuralları zorunlu kılar.
- `Max-Age` istemcinin çerezi ne kadar saklayacağını söyler; kaydın geçerliliğine sunucu
  karar verir ve ikisi ayrı ömürlerdir.
- Yetki düzeyinin değiştiği her noktada oturum kimliği yenilenir; yenilenmeyen kurulumda
  giriş öncesinde bilinen bir değer giriş sonrasında yetkili kayda işaret eder.
- Kayıt sunucuda durduğu için çıkış anında etkilidir; boşta kalma sınırı son istekten,
  mutlak ömür ise kaydın açılışından ölçülür.
- Deponun bedeli her istekte bir okuma ve yatay ölçeklemede paylaşılan bir depo
  gereksinimidir; karşılığında açık oturumların sayılabilmesi ve tek tek kapatılabilmesidir.

#### Sonraki Adım

Deponun bedeli, onu ortadan kaldırma isteğini doğurur. Sunucu kimliği bir kayda çevirmek
yerine, asılın kendisini isteğin taşıdığı değerin **içine** yazabilir ve o değeri imzalayarak
değiştirilmediğinden emin olabilir. Böyle bir kurulumda doğrulama yerel bir hesaba iner,
paylaşılan depo gereksinimi ortadan kalkar ve servisler birbirinin oturum deposuna
bağlanmadan aynı kimliği tanıyabilir. Sonraki ders bu kurulumu yazar ve bedelini ölçer:
sunucuda kayıt yoksa çıkış neyi siler ve silinmeyen bir değer ne kadar süre geçerli kalır?

### Belirteç Tabanlı Kimlik Doğrulama

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/belirtec-tabanli-kimlik-dogrulama

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.

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.

```js
// 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.

```bash
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.

```js
// 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));
```

```bash
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.

```js
// 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());
```

```bash
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.

### JSON Web Belirteçleri

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/json-web-belirtecleri

İmzalı belirtecin standart biçimi: üç parçalı sıkıştırılmış serileştirme, kayıtlı iddiaların anlamı, doğrulayıcının atlamaması gereken adımlar, algoritmanın belirtece değil yapılandırmaya sorulması, saat kayması payı ve imzalama anahtarının uzunluğu.

Önceki dersin belirteç biçimi o derse özgüydü: gövde, nokta, imza. Aynı işi yapan yaygın bir
standart vardır — **JSON web belirteci** (JSON Web Token, JWT) — ve alanların adlarını, imza
algoritmasının nasıl bildirileceğini, sürenin hangi birimle yazılacağını sabitler. Biçimin
standartlaşmasının asıl sonucu doğrulamanın standartlaşmasıdır: bir belirteci kabul etmeden
önce hangi soruların sorulacağı da tanımlıdır.

Bu ders belirteci elle üretir ve elle doğrular. İki doğrulayıcı yazılır: biri beklediği
algoritmayı kendi yapılandırmasından alır, diğeri belirtecin başlığında yazana bakar. İkisinin
aynı belirteçler için verdiği kararlar yan yana konduğunda, doğrulamanın hangi adımının
vazgeçilmez olduğu ölçülebilir hâle gelir.

#### Üç Parça

Belirteç, nokta ile ayrılmış üç base64url parçasından oluşur: başlık, gövde ve imza. İmza,
ilk iki parçanın **kodlanmış hâlleri** üzerine atılır; çözülmüş JSON üzerine değil. Bu ayrım
önemlidir, çünkü aynı JSON nesnesinin farklı boşluk düzenleriyle farklı kodlamaları olur ve
imza belirtecin taşındığı baytları bağlar.

Bu üç parçalı gösterime **sıkıştırılmış serileştirme** (compact serialization) denir; imzalı
veriyi tanımlayan JWS standardının başlıkta, sorgu dizgesinde ve gövdede taşınabilecek
biçimidir.

```js
// jwt.mjs — JWS sikistirilmis serilestirmesinin uretimi ve iki ayri dogrulayici
import { createHmac, timingSafeEqual } from "node:crypto";

export const b64 = (v) => Buffer.from(v).toString("base64url");
export const b64Coz = (v) => Buffer.from(v, "base64url").toString("utf8");

const HMAC = { HS256: "sha256", HS384: "sha384", HS512: "sha512" };

const imzala = (alg, veri, anahtar) =>
  alg === "none" ? "" : createHmac(HMAC[alg], anahtar).update(veri).digest("base64url");

// Belirtec uretimi: baslik ve govde base64url ile kodlanir, imza ikisinin uzerine atilir.
export const uret = ({ alg = "HS256", kid, iddialar, anahtar }) => {
  const baslik = b64(JSON.stringify(kid ? { alg, typ: "JWT", kid } : { alg, typ: "JWT" }));
  const govde = b64(JSON.stringify(iddialar));
  return `${baslik}.${govde}.${imzala(alg, `${baslik}.${govde}`, anahtar)}`;
};

const esitMi = (a, b) => {
  const x = Buffer.from(a, "utf8"), y = Buffer.from(b, "utf8");
  return x.length === y.length && timingSafeEqual(x, y);
};

// DOGRU DOGRULAYICI: beklenen algoritmayi cagiran sabitler, baslik yalniz kid icin okunur.
export const dogrula = (belirtec, { anahtar, beklenenAlg, yayinci, hedefKitle, kaymaSn = 0 }) => {
  const parcalar = (belirtec ?? "").split(".");
  if (parcalar.length !== 3) return { hata: "bicim" };
  const [baslikB64, govdeB64, imza] = parcalar;

  let baslik, iddialar;
  try {
    baslik = JSON.parse(b64Coz(baslikB64));
    iddialar = JSON.parse(b64Coz(govdeB64));
  } catch { return { hata: "cozulemedi" }; }

  // 1. Algoritma dogrulayicinin yapilandirmasindan gelir; belirtecin bildirdiginden degil.
  if (baslik.alg !== beklenenAlg) return { hata: "alg" };

  // 2. Imza, beklenen algoritmayla yeniden hesaplanir.
  if (!esitMi(imza, imzala(beklenenAlg, `${baslikB64}.${govdeB64}`, anahtar))) return { hata: "imza" };

  // 3. Zaman iddialari (saniye cinsinden), saat kaymasi payiyla.
  const simdi = Math.floor(Date.now() / 1000);
  if (typeof iddialar.exp !== "number") return { hata: "exp yok" };
  if (simdi >= iddialar.exp + kaymaSn) return { hata: "suresi gecmis" };
  if (typeof iddialar.nbf === "number" && simdi < iddialar.nbf - kaymaSn) return { hata: "henuz gecerli degil" };

  // 4. Yayinci ve hedef kitle.
  if (iddialar.iss !== yayinci) return { hata: "yayinci" };
  const kitle = Array.isArray(iddialar.aud) ? iddialar.aud : [iddialar.aud];
  if (!kitle.includes(hedefKitle)) return { hata: "hedef kitle" };

  return { iddialar };
};

// YANLIS DOGRULAYICI: algoritmayi belirtecin basligindan okur. Karsi ornek olarak durur.
export const dogrulaBasligaGuvenerek = (belirtec, { anahtar }) => {
  const [baslikB64, govdeB64, imza] = (belirtec ?? "").split(".");
  let baslik, iddialar;
  try {
    baslik = JSON.parse(b64Coz(baslikB64));
    iddialar = JSON.parse(b64Coz(govdeB64));
  } catch { return { hata: "cozulemedi" }; }
  if (!esitMi(imza ?? "", imzala(baslik.alg, `${baslikB64}.${govdeB64}`, anahtar))) return { hata: "imza" };
  return { iddialar };
};
```

Üretilen belirtecin parçaları doğrudan okunabilir.

```js
// yapi.mjs — belirtecin uc parcasi ve iddialarin okunmasi
// Kullanim: node yapi.mjs   (jwt.mjs ile ayni dizinde)
import { uret, b64Coz } from "./jwt.mjs";

const ANAHTAR = Buffer.from("bu-anahtar-yalniz-bu-derste-kullanilir-32b", "utf8");
const simdi = 1785000000;                     // ornek sabit zaman damgasi (saniye)

const belirtec = uret({
  alg: "HS256",
  kid: "2026-01",
  anahtar: ANAHTAR,
  iddialar: {
    iss: "https://kimlik.kutuphane.ornek",    // yayinci
    sub: "U-1001",                            // konu: belirtecin anlattigi asil
    aud: "odunc-servisi",                     // hedef kitle: belirteci kabul edecek servis
    iat: simdi,                               // uretim ani
    nbf: simdi,                               // bu andan once gecerli degil
    exp: simdi + 900,                         // 15 dakika
    jti: "b-000001",                          // belirtec kimligi
    rol: "uye",                               // servise ozgu iddia
  },
});

const [baslik, govde, imza] = belirtec.split(".");
console.log("belirteç uzunluğu :", belirtec.length, "karakter");
console.log("başlık  :", b64Coz(baslik));
console.log("gövde   :", b64Coz(govde));
console.log("imza    :", imza, `(${Buffer.from(imza, "base64url").length} bayt)`);
console.log("imzanın hesaplandığı veri:", `${baslik.slice(0, 12)}….${govde.slice(0, 12)}…`);
```

```
belirteç uzunluğu : 313 karakter
başlık  : {"alg":"HS256","typ":"JWT","kid":"2026-01"}
gövde   : {"iss":"https://kimlik.kutuphane.ornek","sub":"U-1001","aud":"odunc-servisi","iat":1785000000,"nbf":1785000000,"exp":1785000900,"jti":"b-000001","rol":"uye"}
imza    : TLNdRXO5CI9Y6Q-xpcfLsiWwBaKKzxVB3YIaJG070wU (32 bayt)
imzanın hesaplandığı veri: eyJhbGciOiJI….eyJpc3MiOiJo…
```

Belirteç 313 karakterdir ve her istekte bu uzunlukta bir başlık taşınır. Gövdeye eklenen her
iddia bu sayıyı büyütür; belirteç bir veri taşıyıcısı değil, bir kimlik kanıtıdır.

#### Kayıtlı İddialar

Gövdedeki alanlara **iddia** (claim) denir. Bir bölümü standartta adlandırılmıştır ve anlamı
sabittir.

- `iss` — belirteci üreten yayıncı. Doğrulayıcı, beklediği yayıncıyı kendi yapılandırmasında
  tutar ve eşleşmeyeni reddeder.
- `sub` — belirtecin anlattığı asıl. Ödünç servisinde bu, üye ya da personel kodudur.
- `aud` — hedef kitle: belirteci kabul etmesi beklenen alıcı. Tek bir dizge ya da dizge
  dizisi olabilir.
- `exp` — geçerliliğin bittiği an; `nbf` — geçerliliğin başladığı an; `iat` — üretim anı. Üçü
  de 1970 başlangıçlı **saniye** cinsindendir. JavaScript'in milisaniye tabanlı damgasıyla
  karıştırılması, bin kat uzun ya da kısa ömürlü belirteç üretir.
- `jti` — belirteç kimliği. İptal listesi ve yeniden kullanım saptaması bu alana dayanır.

`aud` iddiası, doğrulamada en sık atlanan alandır ve atlanması en somut sonucu doğurur. Ödünç
servisi için üretilmiş bir belirteç, `aud` denetlenmediğinde ceza servisinde de geçer. İki
servis aynı yayıncıya güveniyorsa aralarındaki tek sınır bu alandır.

Standartta yeri olmayan iddialar da yazılabilir — yukarıdaki `rol` gibi. Bunların çakışmaması
için ad alanı ayırmak gerekir; ayrıca önceki derste kurulan kural burada da geçerlidir: gövde
okunabilir olduğu için sır yazılmaz.

#### Sekiz Denemenin Kararı

Aşağıdaki dizge, sekiz farklı belirteci iki doğrulayıcıya sorar. Belirteçlerin hepsi bu
dosyanın kendi ürettiği deneme değerleridir.

```js
// dogrulama-dizgesi.mjs — sekiz deneme belirtecinin iki dogrulayicidan aldigi karar
// Kullanim: node dogrulama-dizgesi.mjs   (jwt.mjs ile ayni dizinde)
import { uret, dogrula, dogrulaBasligaGuvenerek, b64 } from "./jwt.mjs";

const ANAHTAR = Buffer.from("bu-anahtar-yalniz-bu-derste-kullanilir-32b", "utf8");
const BASKA_ANAHTAR = Buffer.from("bu-anahtar-baska-bir-servise-aittir-32bayt", "utf8");
const simdi = Math.floor(Date.now() / 1000);

const taban = {
  iss: "https://kimlik.kutuphane.ornek", sub: "U-1001", aud: "odunc-servisi",
  iat: simdi, nbf: simdi, exp: simdi + 900, jti: "b-000001", rol: "uye",
};
const yap = (deg, alg = "HS256", anahtar = ANAHTAR) => uret({ alg, anahtar, iddialar: { ...taban, ...deg } });

// Govdesi degistirilmis belirtec: imza oldugu gibi birakilir.
const saglam = yap({});
const [b, , i] = saglam.split(".");
const govdesiDegisik = `${b}.${b64(JSON.stringify({ ...taban, rol: "personel" }))}.${i}`;

const DENEMELER = [
  ["geçerli", saglam],
  ["süresi geçmiş", yap({ exp: simdi - 30 })],
  ["henüz geçerli değil", yap({ nbf: simdi + 60 })],
  ["hedef kitle başka servis", yap({ aud: "ceza-servisi" })],
  ["yayıncı başka", yap({ iss: "https://baska.ornek" })],
  ["gövdesi değiştirilmiş", govdesiDegisik],
  ["başlıkta alg: none", yap({}, "none", ANAHTAR)],
  ["başka anahtarla imzalı", yap({}, "HS256", BASKA_ANAHTAR)],
];

const AYAR = {
  anahtar: ANAHTAR, beklenenAlg: "HS256",
  yayinci: "https://kimlik.kutuphane.ornek", hedefKitle: "odunc-servisi",
};

const yaz = (sonuc) => (sonuc.hata ? `ret (${sonuc.hata})` : `kabul (rol=${sonuc.iddialar.rol})`);

console.log("deneme".padEnd(26) + "algoritmayı sabitleyen".padEnd(26) + "başlığa güvenen");
for (const [ad, belirtec] of DENEMELER) {
  console.log(ad.padEnd(26) + yaz(dogrula(belirtec, AYAR)).padEnd(26) + yaz(dogrulaBasligaGuvenerek(belirtec, AYAR)));
}
```

```
deneme                    algoritmayı sabitleyen    başlığa güvenen
geçerli                   kabul (rol=uye)           kabul (rol=uye)
süresi geçmiş             ret (suresi gecmis)       kabul (rol=uye)
henüz geçerli değil       ret (henuz gecerli degil) kabul (rol=uye)
hedef kitle başka servis  ret (hedef kitle)         kabul (rol=uye)
yayıncı başka             ret (yayinci)             kabul (rol=uye)
gövdesi değiştirilmiş     ret (imza)                ret (imza)
başlıkta alg: none        ret (alg)                 kabul (rol=uye)
başka anahtarla imzalı    ret (imza)                ret (imza)
```

Sağdaki sütun yalnız imzayı denetleyen bir doğrulayıcıyı gösteriyor ve sekiz denemenin
altısını kabul ediyor. Süresi geçmiş, henüz geçerli olmayan, başka servis için üretilmiş ve
başka yayıncıdan gelen belirteçler geçiyor; çünkü imza doğrulaması bu soruların hiçbirini
sormaz. İmza "bu belirteç değiştirilmedi" der; "bu belirteç sana, şimdi ve bu yayıncıdan
geldi" demez. Doğrulama, imzayla **başlar** ama imzayla bitmez.

Yedinci satır ayrı bir kural üretir. `alg` alanı belirtecin başlığındadır, yani belirteci
üretenin yazdığı bir değerdir. Doğrulayıcı bu değeri okuyup ona göre davranırsa, hangi
denetimin uygulanacağına belirtecin kendisi karar vermiş olur. Sağdaki doğrulayıcı `none`
bildiren belirteci kabul ediyor: imza boştur, hesaplanan imza da boştur, karşılaştırma
geçer. Soldaki doğrulayıcı aynı belirteci daha ilk adımda reddeder, çünkü beklediği algoritma
yapılandırmasında yazılıdır.

Kural şudur: doğrulayıcı beklediği algoritmayı **kendi yapılandırmasından** alır ve başlıktaki
`alg` alanını yalnız uyuşmazlığı saptamak için okur. Başlığın `kid` alanı bundan ayrıdır:
`kid` hangi anahtarın kullanılacağını söyler ve doğrulayıcının **tanıdığı** anahtarlar
arasından seçim yapar; tanımadığı bir anahtara yönlendiremez.

#### Saat Kayması Payı

Zaman iddiaları iki ayrı makinenin saatine bağlıdır ve saatler tam olarak eşitlenmez. Katı
bir doğrulayıcı, birkaç saniye ileri giden bir üreticinin yeni ürettiği belirteci "henüz
geçerli değil" diye reddedebilir. Bu yüzden doğrulayıcılar küçük bir **saat kayması payı**
tanır.

```js
// kayma.mjs — saat kaymasi payinin dogrulama kararina etkisi
// Kullanim: node kayma.mjs   (jwt.mjs ile ayni dizinde)
import { uret, dogrula } from "./jwt.mjs";

const ANAHTAR = Buffer.from("bu-anahtar-yalniz-bu-derste-kullanilir-32b", "utf8");
const simdi = Math.floor(Date.now() / 1000);
const AYAR = {
  anahtar: ANAHTAR, beklenenAlg: "HS256",
  yayinci: "https://kimlik.kutuphane.ornek", hedefKitle: "odunc-servisi",
};
const iddialar = (deg) => ({
  iss: "https://kimlik.kutuphane.ornek", sub: "U-1001", aud: "odunc-servisi",
  iat: simdi, exp: simdi + 900, ...deg,
});

const sor = (belirtec, kaymaSn) => {
  const s = dogrula(belirtec, { ...AYAR, kaymaSn });
  return s.hata ? `ret (${s.hata})` : "kabul";
};

const suresiGecmis = uret({ anahtar: ANAHTAR, iddialar: iddialar({ exp: simdi - 2 }) });
const henuzGecerliDegil = uret({ anahtar: ANAHTAR, iddialar: iddialar({ nbf: simdi + 2 }) });

console.log("pay".padEnd(8) + "2 sn önce süresi dolmuş".padEnd(28) + "2 sn sonra geçerli olacak");
for (const kayma of [0, 5, 60]) {
  console.log(`${kayma} sn`.padEnd(8) + sor(suresiGecmis, kayma).padEnd(28) + sor(henuzGecerliDegil, kayma));
}
```

```
pay     2 sn önce süresi dolmuş     2 sn sonra geçerli olacak
0 sn    ret (suresi gecmis)         ret (henuz gecerli degil)
5 sn    kabul                       kabul
60 sn   kabul                       kabul
```

Pay iki yönde de çalışır ve belirtecin fiili ömrünü uzatır. Beş saniyelik bir pay, on beş
dakikalık belirteci on beş dakika beş saniye geçerli kılar; altmış saniyelik pay, kısa ömürlü
belirteç kurmanın anlamını aşındırmaya başlar. Ölçü, saat eşitleme hatasının büyüklüğüdür:
birkaç saniye yeterlidir, dakikalar bir yapılandırma hatasını örtmek için kullanılıyor
demektir.

#### Anahtarın Uzunluğu ve Sahibi

HMAC ile imzalamada anahtar bir sırdır ve onu bilen herkes geçerli belirteç üretebilir. Bu
yüzden imzalayan ile doğrulayan aynı sırrı paylaşır. Anahtarın uzunluğu doğrulamanın
çalışmasını etkilemez; yalnızca dayanıklılığı belirler.

```js
// anahtar.mjs — HMAC anahtarinin uzunlugunun dogrulamayi engellemedigi, ama dayanikligi belirledigi
import { createHmac } from "node:crypto";

for (const anahtar of ["1234", "kutuphane", "bu-anahtar-yalniz-bu-derste-kullanilir-32b"]) {
  const bayt = Buffer.from(anahtar, "utf8").length;
  const imza = createHmac("sha256", anahtar).update("baslik.govde").digest("base64url");
  console.log(`${bayt.toString().padStart(2)} bayt anahtar → imza ${imza.slice(0, 16)}… (uzunluk ${Buffer.from(imza, "base64url").length} bayt)`);
}
```

```
 4 bayt anahtar → imza ATOF6UVSe9E4W95T… (uzunluk 32 bayt)
 9 bayt anahtar → imza -pmpkxXgviiFGFpW… (uzunluk 32 bayt)
42 bayt anahtar → imza GK0vNqB99PV6Lz-9… (uzunluk 32 bayt)
```

Üç imza da 32 bayttır ve hiçbiri anahtarın zayıf olduğunu belli etmez. Dört baytlık bir
anahtarla üretilen belirteç, doğrulamadan sorunsuz geçer; sorun doğrulamada değil, anahtarın
denenerek bulunabilmesindedir. Bu yüzden anahtar uzunluğu bir çalışma zamanı denetimi değil,
bir yapılandırma kuralıdır: HMAC anahtarı en az özet uzunluğu kadar — SHA-256 için 32 bayt —
rastgele veriden üretilir. Sözlükte geçen bir parola, uzunluğu tutsa bile bu koşulu
sağlamaz.

Paylaşılan sırrın ikinci sonucu ölçek sorunudur. Ödünç, katalog ve ceza servisleri aynı
belirteci doğruluyorsa üçü de aynı sırrı bilir; üçünden birinin sızması, belirteç üretme
gücünün sızması demektir. Asimetrik imzada bu ayrılır: yayıncı özel anahtarla imzalar,
doğrulayanlar yalnız açık anahtarı bilir ve açık anahtarla belirteç üretilemez. Başlıktaki
`kid` alanı da burada anlamlanır — yayıncı birden çok anahtar kullanabilir ve doğrulayan,
belirtecin hangi anahtarla imzalandığını bu alandan öğrenip kendi tanıdığı anahtarlar
arasından seçer.

#### Özet

- JSON web belirteci nokta ile ayrılmış üç base64url parçadır; imza, başlık ve gövdenin
  kodlanmış hâlleri üzerine atılır.
- Kayıtlı iddialar sabit anlamlıdır: `iss` yayıncıyı, `sub` asılı, `aud` hedef kitleyi,
  `exp`/`nbf`/`iat` saniye cinsinden zamanları, `jti` belirteç kimliğini taşır.
- İmza doğrulaması yalnız değiştirilmemişliği söyler; süre, yayıncı ve hedef kitle
  denetimleri ayrı adımlardır ve atlandıklarında başka servis için üretilmiş ya da süresi
  geçmiş belirteçler kabul edilir.
- Doğrulayıcı beklediği algoritmayı kendi yapılandırmasından alır; `alg` alanını okuyup ona
  göre davranan doğrulayıcı, hangi denetimin uygulanacağını belirtece bırakmış olur.
- Saat kayması payı belirtecin fiili ömrünü uzatır; birkaç saniye saat eşitleme hatası
  içindir, dakikalar bir yapılandırma sorununu örter.
- HMAC anahtarının uzunluğu doğrulamayı etkilemez, dayanıklılığı belirler; paylaşılan sır
  bütün doğrulayıcılara belirteç üretme gücü verir, asimetrik imza bu gücü yayıncıda tutar.

#### Sonraki Adım

Önceki dersin ölçümü, durumsuz belirteçte iptal penceresini belirleyen değişkenin ömür
olduğunu göstermişti. Bu ders ömrü `exp` alanına bağladı ama ikilem duruyor: kısa ömür iptal
penceresini daraltır, buna karşılık kullanıcıyı sık sık yeniden giriş yapmaya zorlar. Çözüm,
tek bir belirteç yerine iki belirteç kullanmaktır — biri kısa ömürlü ve her istekte giden,
diğeri uzun ömürlü ve yalnız yenileme için kullanılan. Sonraki ders bu ikiliyi kurar, uzun
ömürlü belirtecin her kullanımda değiştirilmesinin niçin gerektiğini ve aynı belirtecin ikinci
kez kullanılmasının nasıl saptanabildiğini çalıştırarak gösterir.

### Yenileme Belirteçleri

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/yenileme-belirtecleri

Kısa ömürlü erişim belirteci ile uzun ömürlü yenileme belirtecinin ayrılması: yenileme belirtecinin yalnız karma olarak saklanması, her kullanımda dönüştürülmesi ve kullanılmış bir belirtecin yeniden sunulmasının aile iptaliyle sonuçlanmasının ölçülmesi.

Önceki iki ders bir ikilem bıraktı. Durumsuz belirteçte iptal penceresini ömür belirliyordu:
ömür kısaldıkça pencere daralıyor, ama kullanıcı sık sık yeniden giriş yapmak zorunda
kalıyordu. İkilem, tek belirteç varsayımından doğar. İki belirteç kullanıldığında ömür ile
kullanım kolaylığı ayrı ayrı ayarlanabilir.

**Erişim belirteci** (access token) kısa ömürlüdür ve her isteğe iliştirilir. **Yenileme
belirteci** (refresh token) uzun ömürlüdür, yalnız tek bir uca — yenileme ucuna — gider ve
karşılığında taze bir erişim belirteci verir. Bu ayrımın kendisi bir kazanç sağlamaz;
kazanç, uzun ömürlü olanın sunucuda kayıtlı olmasından ve her kullanımda değiştirilmesinden
gelir.

#### İki Belirteç, İki Görev

Erişim belirteci her istekte gider; dolayısıyla vekil sunucu, günlük, hata izleme kaydı gibi
bütün ara duraklardan geçer. Yenileme belirteci yalnız yenileme ucuna gider; geçtiği yol
kısadır ve o yol dışında hiçbir yerde görünmez.

İkincisi durumludur. Erişim belirteci imzasıyla doğrulanır ve hiçbir kayda bakılmaz;
yenileme belirteci ise sunucudaki bir satıra karşılık gelir. Bu, önceki dersteki
durumsuzluğun terk edilmesi değildir: kayıt okuması dakikada bir olan yenileme isteğinde
yapılır, saniyede yüzlerce olan kaynak isteğinde değil.

Belirtecin istemci tarafında nereye konacağı ayrı bir karardır ve Uygulama Mimarisi kursunun
Tarayıcıda Kimlik Doğrulama dersinde ele alınmıştır; bu ders sunucu tarafını kurar.

#### Yenileme Belirtecinin Saklanması

Yenileme belirteci sunucuda **düz olarak saklanmaz**. Saklanan değer, belirtecin karmasıdır;
belirtecin kendisi yalnız istemcidedir. Belirteç iki parçadan oluşur: aramada kullanılan bir
kimlik ve doğrulamada kullanılan bir sır. Kimlik parçası, bütün satırları tek tek denemeden
doğrudan satıra ulaşmayı sağlar.

```js
// saklama.mjs — yenileme belirtecinin sunucuda hangi bicimde durdugu
// Kullanim: node saklama.mjs
import { randomBytes, createHash } from "node:crypto";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync(":memory:");
db.exec(`CREATE TABLE yenileme (kimlik TEXT PRIMARY KEY, aile TEXT NOT NULL, karma TEXT NOT NULL,
                                uretim INTEGER NOT NULL, kullanim INTEGER)`);

const karma = (sir) => createHash("sha256").update(sir).digest("base64url");

const kimlik = randomBytes(9).toString("base64url");
const sir = randomBytes(32).toString("base64url");
const istemciyeVerilen = `${kimlik}.${sir}`;
db.prepare("INSERT INTO yenileme (kimlik, aile, karma, uretim) VALUES (?, ?, ?, ?)")
  .run(kimlik, "A-1", karma(sir), 1785000000000);

const satir = db.prepare("SELECT * FROM yenileme WHERE kimlik = ?").get(kimlik);
console.log("istemciye verilen :", istemciyeVerilen);
console.log("tablodaki satır   :", JSON.stringify(satir));
console.log("satırda sır var mı:", JSON.stringify(satir).includes(sir));
console.log("sunulan değer doğrulanabiliyor mu:", karma(sir) === satir.karma);
```

```
istemciye verilen : eYxdnXk2HLZc.1wAt5gNZ-E91wUNx_xjYpsdxA7eS7AxYLHqWuQefbCY
tablodaki satır   : {"kimlik":"eYxdnXk2HLZc","aile":"A-1","karma":"SeGl3OEK1MKA2ifVK51meydjt7F-1iABmT01wPg0C3o","uretim":1785000000000,"kullanim":null}
satırda sır var mı: false
sunulan değer doğrulanabiliyor mu: true
```

Kimlik ve sır her çalıştırmada değişir. Satırda sır bulunmuyor; buna karşılık istemcinin
sunduğu değer doğrulanabiliyor. Veritabanının okunması, geçerli yenileme belirteçlerini
vermez.

Burada kullanılan tek geçişli özet, parola saklamada yeterli olmaz. Fark girdinin
öngörülebilirliğindedir: sır 32 bayt rastgele veriden üretilmiştir ve denenerek bulunması
söz konusu değildir. Parolalar için bu koşul sağlanmadığından ayrı bir yöntem gerekir; o
konu ayrıca ele alınacaktır.

#### Yenileme Ucu

Aşağıdaki sunucu iki belirteci de üretir, yenileme ucunu kurar ve bir bayrakla dönüşümsüz
kurulumu da üretebilir. Yenileme belirteçleri bir **aileye** (family) bağlıdır: bir girişte
açılan aile, o girişten türeyen bütün yenileme belirteçlerini kapsar.

```js
// yenileme.mjs — kisa omurlu erisim belirteci + uzun omurlu yenileme belirteci
// Kullanim: node yenileme.mjs <port> [dondurmesiz]
//   dondurmesiz: yenilemede ayni yenileme belirteci gecerli kalir (karsi ornek)
// Ortam: ERISIM_MS (erisim belirteci omru, ontanimli 3000)
import { createServer } from "node:http";
import { createHmac, randomBytes, timingSafeEqual, createHash } from "node:crypto";
import { DatabaseSync } from "node:sqlite";

const PORT = Number(process.argv[2] ?? 8541);
const DONDURMESIZ = process.argv.includes("dondurmesiz");
const ERISIM_MS = Number(process.env.ERISIM_MS ?? 3000);
const AILE_OMRU_MS = 30 * 24 * 3_600_000;              // ailenin mutlak omru

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

const db = new DatabaseSync(":memory:");
db.exec(`
  CREATE TABLE aile (kod TEXT PRIMARY KEY, uye TEXT NOT NULL, acilis INTEGER NOT NULL,
                     iptal INTEGER NOT NULL DEFAULT 0, iptalNedeni TEXT);
  CREATE TABLE yenileme (kimlik TEXT PRIMARY KEY, aile TEXT NOT NULL, karma TEXT NOT NULL,
                         uretim INTEGER NOT NULL, kullanim INTEGER);
`);

// Yenileme belirteci: "<kimlik>.<sir>". Sunucuda yalniz sirrin karmasi durur.
const karma = (sir) => createHash("sha256").update(sir).digest("base64url");
const yenilemeUret = (aile) => {
  const kimlik = randomBytes(9).toString("base64url");
  const sir = randomBytes(32).toString("base64url");
  db.prepare("INSERT INTO yenileme (kimlik, aile, karma, uretim) VALUES (?, ?, ?, ?)")
    .run(kimlik, aile, karma(sir), Date.now());
  return `${kimlik}.${sir}`;
};

const b64 = (v) => Buffer.from(v).toString("base64url");
const erisimUret = (asil) => {
  const govde = b64(JSON.stringify({ ...asil, exp: Date.now() + ERISIM_MS }));
  return `${govde}.${createHmac("sha256", ANAHTAR).update(govde).digest("base64url")}`;
};
const erisimDogrula = (belirtec) => {
  const [govde, imza] = (belirtec ?? "").split(".");
  if (!govde || !imza) return null;
  const beklenen = createHmac("sha256", ANAHTAR).update(govde).digest("base64url");
  const a = Buffer.from(beklenen, "utf8"), b = Buffer.from(imza, "utf8");
  if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
  const icerik = JSON.parse(Buffer.from(govde, "base64url").toString("utf8"));
  return Date.now() < icerik.exp ? icerik : null;
};

const aileIptal = (kod, neden) =>
  db.prepare("UPDATE aile SET iptal = 1, iptalNedeni = ? WHERE kod = ?").run(neden, kod);

// Yenileme belirtecinin degerlendirilmesi: dort ayri ret nedeni ayirt edilir.
const yenilemeCoz = (deger) => {
  const [kimlik, sir] = (deger ?? "").split(".");
  const satir = kimlik ? db.prepare("SELECT * FROM yenileme WHERE kimlik = ?").get(kimlik) : undefined;
  if (!satir) return { hata: "taninmiyor" };

  const a = Buffer.from(karma(sir ?? ""), "utf8"), b = Buffer.from(satir.karma, "utf8");
  if (a.length !== b.length || !timingSafeEqual(a, b)) return { hata: "karma" };

  const aile = db.prepare("SELECT * FROM aile WHERE kod = ?").get(satir.aile);
  if (aile.iptal) return { hata: "aile iptal", aile };
  if (Date.now() - aile.acilis > AILE_OMRU_MS) return { hata: "aile omru doldu", aile };

  // Kullanilmis bir belirtec yeniden sunuldu: kopyanin dolasimda oldugunun isareti.
  if (satir.kullanim !== null && !DONDURMESIZ) {
    aileIptal(aile.kod, "yeniden kullanim saptandi");
    return { hata: "yeniden kullanim", aile };
  }
  return { satir, aile };
};

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) => {
    yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
    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" });
    const aile = randomBytes(6).toString("base64url");
    db.prepare("INSERT INTO aile (kod, uye, acilis) VALUES (?, ?, ?)").run(aile, hesap.kod, Date.now());
    return json(200, { erisim: erisimUret({ kod: hesap.kod, rol: hesap.rol }), yenileme: yenilemeUret(aile) });
  }

  if (istek.method === "POST" && yol === "/yenile") {
    const { yenileme } = JSON.parse((await govdeOku(istek)) || "{}");
    const sonuc = yenilemeCoz(yenileme);
    if (sonuc.hata) return json(401, { hata: sonuc.hata });

    const uye = db.prepare("SELECT uye FROM aile WHERE kod = ?").get(sonuc.aile.kod).uye;
    const govde = { erisim: erisimUret({ kod: uye, rol: "uye" }) };
    if (DONDURMESIZ) {
      govde.yenileme = yenileme;                       // ayni belirtec gecerli kalir
    } else {
      db.prepare("UPDATE yenileme SET kullanim = ? WHERE kimlik = ?").run(Date.now(), sonuc.satir.kimlik);
      govde.yenileme = yenilemeUret(sonuc.aile.kod);   // dönüşüm: yeni belirtec
    }
    return json(200, govde);
  }

  if (yol === "/ben") {
    const tasiyici = (istek.headers.authorization ?? "").startsWith("Bearer ")
      ? istek.headers.authorization.slice(7) : null;
    const icerik = erisimDogrula(tasiyici);
    return icerik ? json(200, { kod: icerik.kod }) : json(401, { hata: "erisim belirteci gecersiz" });
  }

  if (yol === "/tanilama") {
    return json(200, {
      aileler: db.prepare("SELECT kod, iptal, iptalNedeni FROM aile").all(),
      yenilemeSayisi: db.prepare("SELECT COUNT(*) n FROM yenileme").get().n,
      kullanilmisSayisi: db.prepare("SELECT COUNT(*) n FROM yenileme WHERE kullanim IS NOT NULL").get().n,
    });
  }
  json(404, { hata: "kaynak yok" });
}).listen(PORT, "127.0.0.1", () => console.log(`yenileme 127.0.0.1:${PORT} dondurmesiz=${DONDURMESIZ}`));
```

#### Erişim Belirtecinin Ömrü

Erişim belirtecinin ömrü üç saniyeye çekildiğinde akış gözlenebilir hâle gelir: belirteç
süresi dolunca 401 alınır, yenileme ucundan taze bir belirteç alınır ve istek yeniden
çalışır.

```js
// erisim-omru.mjs — erisim belirtecinin omru ve yenilemeyle tazelenmesi
// Kullanim: ERISIM_MS=3000 node yenileme.mjs 8543 &  calisirken  node erisim-omru.mjs 8543
const PORT = Number(process.argv[2] ?? 8543);
const TABAN = `http://127.0.0.1:${PORT}`;
const gonder = async (yol, govde) =>
  (await fetch(TABAN + yol, {
    method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(govde),
  })).json();
const durum = async (erisim) =>
  (await fetch(`${TABAN}/ben`, { headers: { authorization: `Bearer ${erisim}` } })).status;

const giris = await gonder("/giris", { kullanici: "deniz.aral", parola: "üç-kitap-1876" });
const bas = Date.now();
console.log("giriş anında /ben        :", await durum(giris.erisim));
await new Promise((c) => setTimeout(c, 3200));
console.log("3,2 sn sonra /ben        :", await durum(giris.erisim),
  `(erişim ömrü 3 sn, geçen ${((Date.now() - bas) / 1000).toFixed(1)} sn)`);

const taze = await gonder("/yenile", { yenileme: giris.yenileme });
console.log("yenilemeden sonra /ben   :", await durum(taze.erisim));
console.log("eski erişim belirteciyle :", await durum(giris.erisim));
```

```bash
ERISIM_MS=3000 node yenileme.mjs 8543 > /dev/null 2>&1 & S=$!
sleep 1.2
node erisim-omru.mjs 8543
kill $S
```

```
giriş anında /ben        : 200
3,2 sn sonra /ben        : 401 (erişim ömrü 3 sn, geçen 3.2 sn)
yenilemeden sonra /ben   : 200
eski erişim belirteciyle : 401
```

Son satır önemlidir: yenileme, eski erişim belirtecini geçerli kılmaz. Her erişim belirteci
kendi `exp` değerine bağlıdır ve süresi dolduğunda geri gelmez. Yenilemenin yaptığı tek şey
yeni bir belirteç üretmektir.

#### Dönüşümlü Kullanım

**Dönüşüm** (rotation), yenileme belirtecinin her kullanımda değiştirilmesidir: sunucu eski
satırı kullanılmış olarak işaretler ve yeni bir belirteç üretir. Bunun sonucu, her yenileme
belirtecinin **tek kullanımlık** olmasıdır.

Tek kullanımlık olmanın ölçülebilir kazancı, kullanılmış bir belirtecin yeniden sunulduğunda
bir **işaret** taşımasıdır. Normal bir istemci bunu yapmaz; çünkü elindeki belirteci
kullandığında yenisini almıştır. Aynı belirtecin ikinci kez sunulması, o belirtecin bir
kopyasının dolaşımda olduğunu söyler.

```js
// donusum.mjs — donusumlu kullanim ve yeniden kullanim saptamasinin olculmesi
// Kullanim: ERISIM_MS=3000 node yenileme.mjs 8541 &              (donusum var)
//           ERISIM_MS=3000 node yenileme.mjs 8542 dondurmesiz &  (donusum yok)
//           node donusum.mjs 8541   /   node donusum.mjs 8542
const PORT = Number(process.argv[2]);
const TABAN = `http://127.0.0.1:${PORT}`;
const gonder = async (yol, govde) =>
  (await fetch(TABAN + yol, {
    method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(govde),
  })).json();

const giris = await gonder("/giris", { kullanici: "deniz.aral", parola: "üç-kitap-1876" });
const ilkYenileme = giris.yenileme;

// Uc kez yenile; her seferinde eldeki yenileme belirtecini kullan.
let elde = ilkYenileme;
for (let i = 1; i <= 3; i++) {
  const c = await gonder("/yenile", { yenileme: elde });
  console.log(`${i}. yenileme:`, c.hata ? `ret (${c.hata})` : "kabul",
    "| yeni yenileme belirteci mi:", !c.hata && c.yenileme !== elde);
  if (!c.hata) elde = c.yenileme;
}

// Ilk yenileme belirteci yeniden sunuluyor (bir kopyanin dolasimda oldugu durum).
const tekrar = await gonder("/yenile", { yenileme: ilkYenileme });
console.log("ilk belirteç yeniden  :", tekrar.hata ? `ret (${tekrar.hata})` : "kabul");

// En son gecerli belirtecle yeniden denenir.
const sonrasi = await gonder("/yenile", { yenileme: elde });
console.log("güncel belirteçle     :", sonrasi.hata ? `ret (${sonrasi.hata})` : "kabul");
console.log("tanılama:", JSON.stringify(await (await fetch(`${TABAN}/tanilama`)).json()));
```

```bash
ERISIM_MS=3000 node yenileme.mjs 8541 > /dev/null 2>&1 & DONUSUMLU=$!
ERISIM_MS=3000 node yenileme.mjs 8542 dondurmesiz > /dev/null 2>&1 & DONUSUMSUZ=$!
sleep 1.2
echo "— dönüşümlü kullanım —"
node donusum.mjs 8541
echo "— dönüşüm yok —"
node donusum.mjs 8542
kill $DONUSUMLU $DONUSUMSUZ
```

```
— dönüşümlü kullanım —
1. yenileme: kabul | yeni yenileme belirteci mi: true
2. yenileme: kabul | yeni yenileme belirteci mi: true
3. yenileme: kabul | yeni yenileme belirteci mi: true
ilk belirteç yeniden  : ret (yeniden kullanim)
güncel belirteçle     : ret (aile iptal)
tanılama: {"aileler":[{"kod":"hwmd0rUj","iptal":1,"iptalNedeni":"yeniden kullanim saptandi"}],"yenilemeSayisi":4,"kullanilmisSayisi":3}
— dönüşüm yok —
1. yenileme: kabul | yeni yenileme belirteci mi: false
2. yenileme: kabul | yeni yenileme belirteci mi: false
3. yenileme: kabul | yeni yenileme belirteci mi: false
ilk belirteç yeniden  : kabul
güncel belirteçle     : kabul
tanılama: {"aileler":[{"kod":"0hYwgfbk","iptal":0,"iptalNedeni":null}],"yenilemeSayisi":1,"kullanilmisSayisi":0}
```

Aile kodları her çalıştırmada değişir. İki blok arasındaki fark, dönüşümün ne kazandırdığını
tek başına anlatıyor.

Dönüşümlü kurulumda üç yenilemenin ardından tabloda dört satır vardır ve üçü kullanılmıştır.
İlk belirteç yeniden sunulduğunda sunucu bunu **saptar**: satırın `kullanim` alanı doludur.
Kararın sonucu yalnız o isteği reddetmek değildir — aile iptal edilir ve bir sonraki satırda
görüldüğü gibi **güncel** belirteç de artık çalışmaz. Bunun nedeni, hangi tarafın kopya
olduğunun bilinememesidir: yeniden sunan taraf da, elinde güncel belirteci tutan taraf da
aynı aileye aittir. Güvenli karar, ikisini de düşürüp yeniden giriş istemektir.

Dönüşümsüz kurulumda aynı belirteç dört kez kullanıldı, hepsi kabul edildi ve tabloda tek
satır var; `kullanilmisSayisi` sıfır. Bir kopyanın dolaşımda olup olmadığını gösterecek
hiçbir iz yoktur. Sunucunun saptayamadığı bir durum, sunucunun yanıt veremediği bir durumdur.

#### Yanlış Alarm ve Ailenin Ömrü

Saptamanın bir kusuru vardır: aynı yenileme belirteciyle **eşzamanlı** iki istek de aynı
işareti üretir. Uygulamanın iki sekmesi ya da iki isteği aynı anda 401 alıp aynı belirteçle
yenilemeye giderse, ikincisi kullanılmış bir belirteç sunmuş olur ve aile boşuna iptal
edilir. Çözüm istemci tarafındadır ve Uygulama Mimarisi kursunun Oturum Yenileme dersinde
**tek uçuş** adıyla kurulmuştur: aynı anda yalnız bir yenileme isteği çıkar, diğerleri onun
sonucunu bekler. Sunucu tarafında ise küçük bir hoşgörü penceresi tanınabilir — dönüşümden
sonraki birkaç saniye içinde gelen aynı istek, aynı yeni belirteçle yanıtlanır.

Ailenin bir de mutlak ömrü vardır. Dönüşüm her kullanımda yeni belirteç ürettiği için,
düzenli kullanılan bir aile kendiliğinden hiç bitmez. `AILE_OMRU_MS` bunu sınırlar: ilk
girişten belirli bir süre sonra aile, ne kadar kullanılırsa kullanılsın kapanır ve yeniden
giriş gerekir. Aile kaydı ayrıca çıkışın karşılığıdır: çıkış isteği aileyi iptal eder,
böylece o girişten türeyen bütün yenileme belirteçleri tek işlemle düşer.

Böylece iptal penceresi iki farklı süreye ayrılmış olur. Erişim belirteci için pencere onun
kısa ömrüdür — üç saniye, on beş dakika, ne seçilmişse. Yenileme belirteci içinse pencere
sıfırdır, çünkü karar veritabanındaki bir satırdır.

#### Özet

- Erişim belirteci kısa ömürlüdür ve her isteğe iliştirilir; yenileme belirteci uzun
  ömürlüdür ve yalnız yenileme ucuna gider.
- Yenileme belirteci sunucuda karma olarak saklanır; belirteç, aramada kullanılan bir kimlik
  ile doğrulamada kullanılan bir sırdan oluşur ve veritabanının okunması geçerli belirteç
  vermez.
- Dönüşüm, yenileme belirtecini her kullanımda değiştirir ve onu tek kullanımlık kılar;
  kullanılmış bir belirtecin yeniden sunulması bir kopyanın dolaşımda olduğunun işaretidir.
- Saptama üzerine aile iptal edilir; hangi tarafın kopya olduğu bilinemediği için güncel
  belirteç de düşer.
- Dönüşümsüz kurulumda aynı belirteç sınırsız kullanılır ve yeniden kullanımı gösterecek
  hiçbir iz kalmaz.
- Eşzamanlı yenileme yanlış alarm üretebilir; istemcide tek uçuş, sunucuda kısa bir hoşgörü
  penceresi bunu karşılar. Ailenin mutlak ömrü ve çıkışta iptali, oturumun sonunu belirler.

#### Sonraki Adım

Buraya kadar kimlik doğrulamanın bütün adımları tek bir tarafta geçti: kullanıcı parolasını
ödünç servisine verdi, ödünç servisi belirteci üretti ve yine kendisi doğruladı. Kütüphanenin
ilçe şubelerinin ortak bir üye kaydı kullandığı ya da bir üyenin ödünç geçmişini üçüncü taraf
bir okuma uygulamasına açmak istediği durumda bu düzen çalışmaz: parolanın o uygulamaya
verilmesi gerekirdi. Sonraki ders bu sorunu çözen yetki devri protokolünü kurar — kaynak
sahibi parolasını yalnız kendi kimlik sağlayıcısına verir, uygulamaya ise sınırlı bir yetki
çıkar. Yetki kodu akışı, `state` parametresi ve doğrulayıcı, kendi yerel sunucularınızla
uçtan uca çalıştırılacak.

### OAuth 2 Akışları

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/oauth-2-akislari

Yetki devrinin dört rolle kurulması: yetkilendirme sunucusu, kaynak sunucusu ve istemci ile uçtan uca çalıştırılan yetki kodu akışı, PKCE doğrulayıcısı ve state parametresinin kuralları, kodun tek kullanımlığı ile istemci kimlik bilgisi ve aygıt akışları.

Buraya kadar kimlik doğrulamanın bütün adımları tek tarafta geçti: kullanıcı parolasını ödünç
servisine verdi, ödünç servisi belirteci üretti ve yine kendisi doğruladı. Bir üyenin ödünç
geçmişini üçüncü taraf bir okuma uygulamasına açmak istediği durumda bu düzen çalışmaz —
uygulamaya parolanın verilmesi gerekirdi. Parolayı alan uygulama ise ödünç geçmişini okumakla
kalmaz, üyenin yapabildiği her şeyi yapabilir ve erişimi ancak parola değiştirilerek kesilir.

**OAuth 2**, bu sorunu yetkiyi devrederek çözer. Kaynak sahibi kimlik bilgisini yalnız kendi
yetkilendirme sunucusuna verir; uygulamaya çıkan şey, kapsamı ve ömrü sınırlı bir belirteçtir.

#### Dört Rol

**Kaynak sahibi** (resource owner) yetkiyi veren taraftır — burada üye U-1001. **İstemci**
(client) yetkiyi isteyen uygulamadır. **Yetkilendirme sunucusu** (authorization server)
kaynak sahibini doğrular, onayını alır ve belirteç üretir. **Kaynak sunucusu** (resource
server) belirteci doğrulayıp korunan veriyi verir.

Yetki kodu akışında istemci belirteci doğrudan almaz. Kaynak sahibi yetkilendirme sunucusuna
yönlendirilir, oradan istemciye kısa ömürlü bir **yetki kodu** (authorization code) döner ve
istemci bu kodu, tarayıcıdan geçmeyen ayrı bir istekle belirteçle değiştirir. Belirtecin
tarayıcı adres çubuğunda hiç görünmemesinin nedeni budur.

#### Yetkilendirme Sunucusu

```js
// yetkilendirme-sunucusu.mjs — yetki kodu (PKCE), istemci kimlik bilgisi ve aygit akislari
// Kullanim: node yetkilendirme-sunucusu.mjs [port]     (ontanimli 8551)
import { createServer } from "node:http";
import { createHmac, createHash, randomBytes, timingSafeEqual } from "node:crypto";

const PORT = Number(process.argv[2] ?? 8551);
const YAYINCI = `http://127.0.0.1:${PORT}`;
const ANAHTAR = Buffer.from("kimlik-saglayici-imza-anahtari-32bayt!!!", "utf8");

// "acik" istemci sir saklayamaz (tarayici/mobil), "gizli" istemci saklayabilir.
const ISTEMCILER = new Map([
  ["okuma-uygulamasi", { tur: "acik", yonlendirme: ["http://127.0.0.1:8553/geri"], kapsam: ["odunc:oku"] }],
  ["raf-terminali", { tur: "gizli", sir: "raf-terminali-sirri-1", kapsam: ["odunc:oku", "odunc:yaz"] }],
]);
const KODLAR = new Map(), AYGITLAR = new Map();

const s256 = (v) => createHash("sha256").update(v).digest("base64url");
const esitMi = (a, b) => {
  const x = Buffer.from(a, "utf8"), y = Buffer.from(b, "utf8");
  return x.length === y.length && timingSafeEqual(x, y);
};
const erisimUret = (sub, istemci, kapsam) => {
  const g = Buffer.from(JSON.stringify({ iss: YAYINCI, sub, aud: "odunc-servisi", client_id: istemci,
    scope: kapsam.join(" "), exp: Math.floor(Date.now() / 1000) + 600 })).toString("base64url");
  return `${g}.${createHmac("sha256", ANAHTAR).update(g).digest("base64url")}`;
};
const formOku = (istek) => new Promise((coz) => {
  let v = ""; istek.on("data", (p) => (v += p));
  istek.on("end", () => coz(Object.fromEntries(new URLSearchParams(v))));
});

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, YAYINCI), s = adres.searchParams;
  const json = (kod, govde) => {
    yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
    yanit.end(JSON.stringify(govde));
  };

  if (istek.method === "GET" && adres.pathname === "/yetkilendir") {
    const istemci = ISTEMCILER.get(s.get("client_id") ?? ""), yon = s.get("redirect_uri") ?? "";
    // Istemci ya da yonlendirme adresi taninmiyorsa YONLENDIRME YAPILMAZ; hata burada durur.
    if (!istemci || !(istemci.yonlendirme ?? []).includes(yon)) {
      return json(400, { error: "invalid_request", detay: "istemci ya da yonlendirme adresi taninmiyor" });
    }
    const hataylaDon = (kod) => {
      const u = new URL(yon);
      u.searchParams.set("error", kod);
      if (s.get("state")) u.searchParams.set("state", s.get("state"));
      yanit.writeHead(302, { location: u.toString() }); yanit.end();
    };
    if (s.get("response_type") !== "code" || !s.get("state")) return hataylaDon("invalid_request");
    if (!s.get("code_challenge") || s.get("code_challenge_method") !== "S256") return hataylaDon("invalid_request");
    const istenen = (s.get("scope") ?? "").split(" ").filter(Boolean);
    if (!istenen.length || !istenen.every((k) => istemci.kapsam.includes(k))) return hataylaDon("invalid_scope");

    // Bu noktada kaynak sahibi kimligini dogrulamis ve onay ekranini gecmis sayiliyor.
    const kod = randomBytes(24).toString("base64url");
    KODLAR.set(kod, { client_id: s.get("client_id"), redirect_uri: yon, meydan: s.get("code_challenge"),
      kapsam: istenen, sub: "U-1001", exp: Date.now() + 60_000, kullanildi: false });
    const u = new URL(yon);
    u.searchParams.set("code", kod); u.searchParams.set("state", s.get("state"));
    yanit.writeHead(302, { location: u.toString() });
    return yanit.end();
  }

  if (istek.method === "POST" && adres.pathname === "/aygit/kod") {
    const g = await formOku(istek);
    const aygitKodu = randomBytes(18).toString("base64url"), kullaniciKodu = randomBytes(3).toString("hex").toUpperCase();
    AYGITLAR.set(aygitKodu, { client_id: g.client_id, kullaniciKodu, onay: false, kapsam: (g.scope ?? "").split(" ") });
    return json(200, { device_code: aygitKodu, user_code: kullaniciKodu,
      verification_uri: `${YAYINCI}/aygit`, expires_in: 600, interval: 1 });
  }
  if (istek.method === "POST" && adres.pathname === "/aygit/onay") {
    const g = await formOku(istek);
    for (const [, k] of AYGITLAR) if (k.kullaniciKodu === g.user_code) k.onay = true;
    return json(200, { onaylandi: true });
  }

  if (istek.method === "POST" && adres.pathname === "/belirtec") {
    const g = await formOku(istek);

    if (g.grant_type === "authorization_code") {
      const k = KODLAR.get(g.code ?? "");
      if (!k) return json(400, { error: "invalid_grant", detay: "kod taninmiyor" });
      if (k.kullanildi) { KODLAR.delete(g.code); return json(400, { error: "invalid_grant", detay: "kod ikinci kez sunuldu" }); }
      if (Date.now() > k.exp) return json(400, { error: "invalid_grant", detay: "kodun suresi gecti" });
      if (g.client_id !== k.client_id) return json(400, { error: "invalid_grant", detay: "istemci uyusmuyor" });
      if (g.redirect_uri !== k.redirect_uri) return json(400, { error: "invalid_grant", detay: "yonlendirme uyusmuyor" });
      if (!g.code_verifier || !esitMi(s256(g.code_verifier), k.meydan)) {
        return json(400, { error: "invalid_grant", detay: "dogrulayici uyusmuyor" });
      }
      k.kullanildi = true;
      return json(200, { access_token: erisimUret(k.sub, k.client_id, k.kapsam),
        token_type: "Bearer", expires_in: 600, scope: k.kapsam.join(" ") });
    }

    if (g.grant_type === "client_credentials") {
      const [ad, sir] = Buffer.from((istek.headers.authorization ?? "").replace("Basic ", ""), "base64")
        .toString("utf8").split(":");
      const istemci = ISTEMCILER.get(ad ?? "");
      if (!istemci || istemci.tur !== "gizli" || !esitMi(sir ?? "", istemci.sir)) return json(401, { error: "invalid_client" });
      const istenen = (g.scope ?? "").split(" ").filter(Boolean);
      if (!istenen.every((k) => istemci.kapsam.includes(k))) return json(400, { error: "invalid_scope" });
      return json(200, { access_token: erisimUret(ad, ad, istenen), token_type: "Bearer",
        expires_in: 600, scope: istenen.join(" ") });
    }

    if (g.grant_type === "urn:ietf:params:oauth:grant-type:device_code") {
      const k = AYGITLAR.get(g.device_code ?? "");
      if (!k) return json(400, { error: "invalid_grant" });
      if (!k.onay) return json(400, { error: "authorization_pending" });
      AYGITLAR.delete(g.device_code);
      return json(200, { access_token: erisimUret("U-1001", k.client_id, k.kapsam),
        token_type: "Bearer", expires_in: 600, scope: k.kapsam.join(" ") });
    }
    return json(400, { error: "unsupported_grant_type" });
  }
  json(404, { error: "not_found" });
}).listen(PORT, "127.0.0.1", () => console.log(`yetkilendirme sunucusu ${YAYINCI}`));
```

Yetkilendirme ucundaki denetimlerin sırası rastgele değildir. İstemci ya da yönlendirme adresi
tanınmıyorsa **yönlendirme yapılmaz**: hata, kaynak sahibinin tarayıcısında durur. Yönlendirme
adresinin doğrulanmadığı bir kurulumda hata yanıtı — ve daha kötüsü, yetki kodu — kayıtlı
olmayan bir adrese gönderilebilirdi. Yönlendirme adresi tam eşleşmeyle denetlenir; önek
eşleşmesi ya da joker kabul edilmez.

#### Kaynak Sunucusu

Kaynak sunucusu belirteci doğrular ve uca göre gereken **kapsamı** (scope) arar.

```js
// kaynak-sunucusu.mjs — odunc servisi: belirteci dogrular ve kapsam denetler
// Kullanim: node kaynak-sunucusu.mjs [port]     (ontanimli 8552)
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";

const PORT = Number(process.argv[2] ?? 8552);
const ANAHTAR = Buffer.from("kimlik-saglayici-imza-anahtari-32bayt!!!", "utf8");
const GEREKEN = new Map([["GET", "odunc:oku"], ["POST", "odunc:yaz"]]);

const dogrula = (belirtec) => {
  const [govde, imza] = (belirtec ?? "").split(".");
  if (!govde || !imza) return null;
  const beklenen = createHmac("sha256", ANAHTAR).update(govde).digest("base64url");
  const a = Buffer.from(beklenen, "utf8"), b = Buffer.from(imza, "utf8");
  if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
  const i = JSON.parse(Buffer.from(govde, "base64url").toString("utf8"));
  if (i.iss !== "http://127.0.0.1:8551" || i.aud !== "odunc-servisi") return null;
  return Math.floor(Date.now() / 1000) < i.exp ? i : null;
};

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const json = (kod, govde) => {
    yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
    yanit.end(JSON.stringify(govde));
  };
  const b = istek.headers.authorization ?? "";
  const iddialar = b.startsWith("Bearer ") ? dogrula(b.slice(7)) : null;
  if (!iddialar) return json(401, { error: "invalid_token" });

  const gereken = istek.url.split("?")[0] === "/odunc" ? GEREKEN.get(istek.method) : undefined;
  if (!gereken) return json(404, { error: "not_found" });
  if (!iddialar.scope.split(" ").includes(gereken)) {
    return json(403, { error: "insufficient_scope", gereken, verilen: iddialar.scope });
  }
  json(istek.method === "GET" ? 200 : 201, { sub: iddialar.sub, istemci: iddialar.client_id, kapsam: iddialar.scope });
}).listen(PORT, "127.0.0.1", () => console.log(`kaynak sunucusu http://127.0.0.1:${PORT}`));
```

#### İstemci

İstemci iki değer üretir ve ikisini de kendinde saklar: `state` ve PKCE **doğrulayıcısı**
(code verifier). Yetkilendirme sunucusuna doğrulayıcının kendisi değil, özeti — **meydan
okuma** (code challenge) — gönderilir.

```js
// istemci.mjs — okuma uygulamasi: akisi baslatir, geri donusu karsilar, belirteci alir
// Kullanim: node istemci.mjs [port]     (ontanimli 8553)
import { createServer } from "node:http";
import { createHash, randomBytes } from "node:crypto";

const PORT = Number(process.argv[2] ?? 8553);
const KENDI = `http://127.0.0.1:${PORT}`, AS = "http://127.0.0.1:8551", KAYNAK = "http://127.0.0.1:8552";
const BEKLEYEN = new Map();       // state -> PKCE dogrulayicisi; kayit istemcide durur
const s256 = (v) => createHash("sha256").update(v).digest("base64url");

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, KENDI);
  const json = (kod, govde) => {
    yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
    yanit.end(JSON.stringify(govde));
  };

  if (adres.pathname === "/basla") {
    const state = randomBytes(12).toString("base64url"), dogrulayici = randomBytes(32).toString("base64url");
    BEKLEYEN.set(state, dogrulayici);
    const u = new URL(`${AS}/yetkilendir`);
    for (const [ad, deger] of Object.entries({ response_type: "code", client_id: "okuma-uygulamasi",
      redirect_uri: `${KENDI}/geri`, scope: "odunc:oku", state,
      code_challenge: s256(dogrulayici), code_challenge_method: "S256" })) u.searchParams.set(ad, deger);
    return json(200, { yetkilendirmeAdresi: u.toString() });
  }

  if (adres.pathname === "/geri") {
    const state = adres.searchParams.get("state");
    // 1. state istemcinin kendi kaydiyla eslesmeli; eslesmiyorsa akis buraya ait degildir.
    if (!state || !BEKLEYEN.has(state)) return json(400, { hata: "state eslesmedi" });
    const dogrulayici = BEKLEYEN.get(state);
    BEKLEYEN.delete(state);                                  // state tek kullanimlik
    if (adres.searchParams.get("error")) return json(400, { hata: adres.searchParams.get("error") });

    // 2. Kod, dogrulayiciyla birlikte belirtece cevrilir.
    const cevap = await fetch(`${AS}/belirtec`, {
      method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({ grant_type: "authorization_code", code: adres.searchParams.get("code"),
        redirect_uri: `${KENDI}/geri`, client_id: "okuma-uygulamasi", code_verifier: dogrulayici }),
    });
    const belirtec = await cevap.json();
    if (!cevap.ok) return json(400, { hata: belirtec });

    // 3. Belirtecle kaynak sunucusu cagrilir.
    const kaynak = await fetch(`${KAYNAK}/odunc`, { headers: { authorization: `Bearer ${belirtec.access_token}` } });
    return json(200, { kapsam: belirtec.scope, kaynakDurumu: kaynak.status, kaynakGovdesi: await kaynak.json() });
  }
  json(404, { hata: "kaynak yok" });
}).listen(PORT, "127.0.0.1", () => console.log(`istemci ${KENDI}`));
```

İki değerin işlevi ayrıdır ve ikisi de zorunludur.

`state`, geri dönen isteğin istemcinin **kendi başlattığı** akışa ait olduğunu doğrular.
İstemci `state` değerini üretir, kendinde saklar ve geri dönüşte kayıtlı olup olmadığına
bakar; eşleşmeyen bir geri dönüş, istemcinin başlatmadığı bir akışın sonucudur ve reddedilir.
`state` denetlemeyen bir istemci, dışarıdan başlatılmış bir akışın kodunu kendi kullanıcısının
oturumuna bağlayabilir. Bu yüzden `state` üretilmemişse ya da denetlenmiyorsa kurulum
reddedilmelidir.

PKCE doğrulayıcısı, yetki kodunu **kodu isteyen istemciye** bağlar. Yetkilendirme sunucusu
kodu meydan okumayla birlikte saklar; belirteç isteğinde doğrulayıcının özeti bu meydan
okumayla karşılaştırılır. Doğrulayıcı istemcinin belleğinden hiç çıkmadığı için, kodu başka
bir yerden ele geçiren bir tarafın elinde onu belirteçle değiştirecek parça bulunmaz. Sır
saklayamayan açık istemciler için kodun tek koruması budur; bu yüzden `code_challenge`
gelmiyorsa ya da yöntem `S256` değilse istek reddedilmelidir.

#### Akışın Uçtan Uca Çalıştırılması

Üç sunucu ayağa kaldırılır ve tarayıcının rolünü `curl` oynar: istemciden yetkilendirme
adresini alır, o adrese gider, dönen `Location` başlığını izler.

```bash
node yetkilendirme-sunucusu.mjs > /dev/null 2>&1 & Y=$!
node kaynak-sunucusu.mjs > /dev/null 2>&1 & K=$!
node istemci.mjs > /dev/null 2>&1 & I=$!
sleep 1.2
ADRES=$(curl -s http://127.0.0.1:8553/basla | sed -n 's/.*"yetkilendirmeAdresi":"\([^"]*\)".*/\1/p')
echo "1) yetkilendirme adresi:"; echo "   $ADRES"
GERI=$(curl -s -o /dev/null -w '%{redirect_url}' "$ADRES")
echo "2) geri dönüş adresi:"; echo "   $GERI"
echo "3) istemcinin sonucu:"; curl -s "$GERI"; echo
kill $Y $K $I
```

```
1) yetkilendirme adresi:
   http://127.0.0.1:8551/yetkilendir?response_type=code&client_id=okuma-uygulamasi&redirect_uri=http%3A%2F%2F127.0.0.1%3A8553%2Fgeri&scope=odunc%3Aoku&state=60fJ6tHMZO8KtTWD&code_challenge=upHf2ThVHNISKmkd0VCRtiA0Plr3VPQ0WFAzk1zrG4o&code_challenge_method=S256
2) geri dönüş adresi:
   http://127.0.0.1:8553/geri?code=-GL-lEji0qFimP42TeSl7aHGnEm_jTOh&state=60fJ6tHMZO8KtTWD
3) istemcinin sonucu:
{"kapsam":"odunc:oku","kaynakDurumu":200,"kaynakGovdesi":{"sub":"U-1001","istemci":"okuma-uygulamasi","kapsam":"odunc:oku"}}
```

`state`, meydan okuma ve kod her çalıştırmada değişir. İkinci satırda tarayıcıdan geçen tek
şeyin kod olduğu görülüyor; belirteç hiçbir adreste görünmüyor. Kaynak sunucusunun yanıtında
ise `sub` üyeyi, `istemci` uygulamayı ayrı ayrı bildiriyor: kaynak sunucusu isteğin kimin
adına ve hangi uygulama eliyle geldiğini birbirinden ayırabiliyor.

#### Belirteç Ucundaki Denetimler

Aşağıdaki blok kodu doğrudan belirteç ucunda dener; doğrulayıcı ve meydan okuma kabukta
üretilir.

```bash
node yetkilendirme-sunucusu.mjs > /dev/null 2>&1 & Y=$!
sleep 1
AS=http://127.0.0.1:8551
GERI=http://127.0.0.1:8553/geri
DOGRULAYICI=$(head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=')
MEYDAN=$(printf '%s' "$DOGRULAYICI" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '=')
kodAl() {
  curl -s -o /dev/null -w '%{redirect_url}' \
    "$AS/yetkilendir?response_type=code&client_id=okuma-uygulamasi&redirect_uri=$GERI&scope=odunc:oku&state=abc&code_challenge=$MEYDAN&code_challenge_method=S256" \
    | sed -n 's/.*[?&]code=\([^&]*\).*/\1/p'
}
degis() { curl -s -d "$1" "$AS/belirtec" | sed 's/"access_token":"[^"]*"/"access_token":"…"/'; }
KOD=$(kodAl)
echo "gecerli degisim     : $(degis "grant_type=authorization_code&code=$KOD&redirect_uri=$GERI&client_id=okuma-uygulamasi&code_verifier=$DOGRULAYICI")"
echo "ayni kod ikinci kez : $(degis "grant_type=authorization_code&code=$KOD&redirect_uri=$GERI&client_id=okuma-uygulamasi&code_verifier=$DOGRULAYICI")"
KOD=$(kodAl)
echo "dogrulayici yok     : $(degis "grant_type=authorization_code&code=$KOD&redirect_uri=$GERI&client_id=okuma-uygulamasi")"
KOD=$(kodAl)
echo "baska yonlendirme   : $(degis "grant_type=authorization_code&code=$KOD&redirect_uri=http://127.0.0.1:8553/baska&client_id=okuma-uygulamasi&code_verifier=$DOGRULAYICI")"
kill $Y
```

```
gecerli degisim     : {"access_token":"…","token_type":"Bearer","expires_in":600,"scope":"odunc:oku"}
ayni kod ikinci kez : {"error":"invalid_grant","detay":"kod ikinci kez sunuldu"}
dogrulayici yok     : {"error":"invalid_grant","detay":"dogrulayici uyusmuyor"}
baska yonlendirme   : {"error":"invalid_grant","detay":"yonlendirme uyusmuyor"}
```

Kod tek kullanımlıktır ve ikinci sunuluşunda kayıt tümüyle düşürülür — yenileme belirteçlerinde
kurulan yeniden kullanım saptamasının aynısıdır. Doğrulayıcının bulunmaması ile yanlış bir
doğrulayıcı sunulması aynı yanıtı alır; ayrım yapmak, dışarıya bir bilgi sızdırmaktan başka
bir işe yaramaz.

#### İki Akış Daha

**İstemci kimlik bilgisi akışı** (client credentials), kaynak sahibinin bulunmadığı durum
içindir: raf terminali kendi adına çalışır, kimliğini kendi sırrıyla kanıtlar ve kapsamı kendi
kaydından gelir. **Aygıt akışı** (device flow), tarayıcı açamayan aygıtlar içindir: aygıt bir
kullanıcı kodu alır, kullanıcı bu kodu başka bir aygıtta onaylar, aygıt bu sırada yoklama
yapar.

```bash
node yetkilendirme-sunucusu.mjs > /dev/null 2>&1 & Y=$!
node kaynak-sunucusu.mjs > /dev/null 2>&1 & K=$!
sleep 1
AS=http://127.0.0.1:8551
kisalt() { sed 's/"access_token":"[^"]*"/"access_token":"…"/'; }
YANIT=$(curl -s -u raf-terminali:raf-terminali-sirri-1 -d 'grant_type=client_credentials&scope=odunc:oku odunc:yaz' $AS/belirtec)
echo "$YANIT" | kisalt
BELIRTEC=$(echo "$YANIT" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
echo "POST /odunc: $(curl -s -X POST -H "Authorization: Bearer $BELIRTEC" http://127.0.0.1:8552/odunc)"
YALNIZ=$(curl -s -u raf-terminali:raf-terminali-sirri-1 -d 'grant_type=client_credentials&scope=odunc:oku' $AS/belirtec | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
echo "yalniz okuma kapsamiyla POST /odunc: $(curl -s -X POST -H "Authorization: Bearer $YALNIZ" http://127.0.0.1:8552/odunc)"
AYGIT=$(curl -s -d 'client_id=raf-terminali&scope=odunc:oku' $AS/aygit/kod)
echo "$AYGIT"
AYGIT_KODU=$(echo "$AYGIT" | sed -n 's/.*"device_code":"\([^"]*\)".*/\1/p')
KULLANICI_KODU=$(echo "$AYGIT" | sed -n 's/.*"user_code":"\([^"]*\)".*/\1/p')
echo "onay oncesi yoklama : $(curl -s -d "grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=$AYGIT_KODU" $AS/belirtec)"
curl -s -o /dev/null -d "user_code=$KULLANICI_KODU" $AS/aygit/onay
echo "onay sonrasi yoklama: $(curl -s -d "grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=$AYGIT_KODU" $AS/belirtec | kisalt)"
kill $Y $K
```

```
{"access_token":"…","token_type":"Bearer","expires_in":600,"scope":"odunc:oku odunc:yaz"}
POST /odunc: {"sub":"raf-terminali","istemci":"raf-terminali","kapsam":"odunc:oku odunc:yaz"}
yalniz okuma kapsamiyla POST /odunc: {"error":"insufficient_scope","gereken":"odunc:yaz","verilen":"odunc:oku"}
{"device_code":"EafPu8Lm73tmn2_R8jKGtWd3","user_code":"613D0C","verification_uri":"http://127.0.0.1:8551/aygit","expires_in":600,"interval":1}
onay oncesi yoklama : {"error":"authorization_pending"}
onay sonrasi yoklama: {"access_token":"…","token_type":"Bearer","expires_in":600,"scope":"odunc:oku"}
```

Aygıt ve kullanıcı kodları her çalıştırmada değişir. Üçüncü satır kapsamın işini gösteriyor:
aynı istemci, yalnız okuma kapsamıyla aldığı belirteçle yazma ucunda 403 alıyor. Kapsam,
belirtecin kime ait olduğunu değil, **neye izinli** olduğunu daraltır. Aygıt akışında ise
yoklama, onay gelene kadar `authorization_pending` alır; aygıtın yanıtta bildirilen `interval`
değerinden sık yoklamaması gerekir.

#### Özet

- Yetki devri dört rol tanımlar: kaynak sahibi, istemci, yetkilendirme sunucusu ve kaynak
  sunucusu. Kaynak sahibi kimlik bilgisini yalnız yetkilendirme sunucusuna verir.
- Yetki kodu akışında tarayıcıdan yalnız kısa ömürlü kod geçer; belirteç, tarayıcıdan
  geçmeyen ayrı bir istekle alınır.
- Yönlendirme adresi tam eşleşmeyle denetlenir ve tanınmayan adrese hiçbir şey gönderilmez;
  `state` üretilmemiş ya da denetlenmiyorsa, `code_challenge` yoksa ya da yöntem `S256`
  değilse kurulum reddedilmelidir.
- Yetki kodu tek kullanımlıktır; ikinci sunuluşunda kayıt düşürülür ve doğrulayıcı
  uyuşmazlığı ile eksikliği aynı yanıtı alır.
- İstemci kimlik bilgisi akışı kaynak sahibinin bulunmadığı makine çağrıları içindir; aygıt
  akışı tarayıcı açamayan aygıtlar için onayı başka bir aygıta taşır.
- Kapsam, belirtecin neye izinli olduğunu daraltır; kaynak sunucusu uca göre gereken kapsamı
  arar ve eksikse 403 döner.

#### Sonraki Adım

Bu akışların hiçbiri "bu kullanıcı kim?" sorusunu yanıtlamadı. Kaynak sunucusu belirteçteki
`sub` alanını okudu ama o alanın ne anlama geldiği, hangi kullanıcıya karşılık geldiği ve
belirtecin gerçekten bir kimlik doğrulama sonucunda üretilip üretilmediği yetki devri
protokolünün kapsamı dışındadır. İstemcinin kullanıcıyı tanıması gerekiyorsa — adını
göstermek, hesabını eşleştirmek, oturumunu açmak için — ayrı bir katman gerekir. Sonraki ders
bu katmanı kurar: yetkilendirme sunucusu istemciye erişim belirtecinin yanında bir **kimlik
belirteci** verir ve istemci onu yayıncı, hedef kitle, süre ve imza adımlarıyla doğrular.

### OpenID Connect

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/openid-connect

Yetki devri protokolünün üzerine kimlik katmanının eklenmesi: openid kapsamı ile üretilen kimlik belirteci, erişim belirtecinden hedef kitleyle ayrılması, keşif belgesi ve anahtar kümesi üzerinden asimetrik imza doğrulaması ve nonce ile yayıncı, hedef kitle, süre adımları.

Önceki dersin akışlarının hiçbiri "bu kullanıcı kim?" sorusunu yanıtlamadı. Kaynak sunucusu
belirteçteki `sub` alanını okudu ama o alanın hangi kullanıcıya karşılık geldiği, belirtecin
gerçekten bir kimlik doğrulama sonucunda üretilip üretilmediği yetki devri protokolünün
kapsamı dışındadır. Erişim belirteci "bu istek şu kaynaklara erişebilir" der; "şu kişi
kimliğini az önce doğruladı" demez.

**OpenID Connect**, bu boşluğu yetki devri protokolünün üzerine bir kimlik katmanı ekleyerek
doldurur. İstemci `openid` kapsamını istediğinde, belirteç yanıtına ikinci bir belirteç
eklenir: **kimlik belirteci** (ID token). Bu belirteç kaynak sunucusuna değil, **istemciye**
aittir ve kullanıcının kim olduğunu, ne zaman doğrulandığını bildirir.

#### İki Belirteç, İki Alıcı

Ayrım `aud` iddiasında görünür. Erişim belirtecinin hedef kitlesi kaynak sunucusudur; kimlik
belirtecinin hedef kitlesi istemcinin kendisidir. Bundan iki kural çıkar: istemci kimlik
belirtecini API'ye göndermez, kaynak sunucusu da kimlik belirtecini kabul etmez. İkisi
karıştırıldığında, bir servis için üretilmiş belirteç başka bir servise kanıt olarak sunulmuş
olur.

Kimlik belirteci ayrıca **doğrulanmak üzere** üretilir, taşınmak üzere değil. İstemci onu bir
kez doğrular, içindeki bilgiyi kendi oturumuna yazar ve belirteci saklamak zorunda kalmaz.

#### Sağlayıcı

Aşağıdaki sağlayıcı, önceki dersin yetki kodu akışını kimlik katmanıyla genişletir: `openid`
kapsamı ve `nonce` parametresi zorunludur, imza asimetriktir ve açık anahtarlar bir **anahtar
kümesi** (JSON Web Key Set) ucundan yayımlanır.

```js
// saglayici.mjs — kimlik katmani: kesif belgesi, anahtar kumesi, kimlik belirteci, kullanici ucu
// Kullanim: node saglayici.mjs [port]     (ontanimli 8561)
import { createServer } from "node:http";
import { generateKeyPairSync, createHash, randomBytes, sign, verify, timingSafeEqual } from "node:crypto";

const PORT = Number(process.argv[2] ?? 8561);
const YAYINCI = `http://127.0.0.1:${PORT}`;
const ISTEMCI = { kimlik: "okuma-uygulamasi", yonlendirme: "http://127.0.0.1:8563/geri" };
const KULLANICI = { sub: "U-1001", ad: "Deniz Aral", eposta: "deniz.aral@ornek.kutuphane", sube: "merkez" };

// Iki anahtar: yururlukteki ve emekliye ayrilmis. Ikisi de anahtar kumesinde durur.
const anahtarYap = (kid) => ({ kid, ...generateKeyPairSync("rsa", { modulusLength: 2048 }) });
const ANAHTARLAR = [anahtarYap("2026-a"), anahtarYap("2025-b")];
const YURURLUKTE = ANAHTARLAR[0];

const KODLAR = new Map();
const b64 = (v) => Buffer.from(v).toString("base64url");
const s256 = (v) => createHash("sha256").update(v).digest("base64url");
const esitMi = (a, b) => {
  const x = Buffer.from(a, "utf8"), y = Buffer.from(b, "utf8");
  return x.length === y.length && timingSafeEqual(x, y);
};

const jwsUret = (anahtar, iddialar) => {
  const veri = `${b64(JSON.stringify({ alg: "RS256", typ: "JWT", kid: anahtar.kid }))}.${b64(JSON.stringify(iddialar))}`;
  return `${veri}.${sign("sha256", Buffer.from(veri), anahtar.privateKey).toString("base64url")}`;
};

const jwsDogrula = (belirtec) => {
  const [b, g, i] = (belirtec ?? "").split(".");
  if (!b || !g || !i) return null;
  const baslik = JSON.parse(Buffer.from(b, "base64url").toString("utf8"));
  const anahtar = ANAHTARLAR.find((a) => a.kid === baslik.kid);
  if (!anahtar || baslik.alg !== "RS256") return null;
  if (!verify("sha256", Buffer.from(`${b}.${g}`), anahtar.publicKey, Buffer.from(i, "base64url"))) return null;
  const iddialar = JSON.parse(Buffer.from(g, "base64url").toString("utf8"));
  return Math.floor(Date.now() / 1000) < iddialar.exp ? iddialar : null;
};

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

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, YAYINCI), s = adres.searchParams;
  const json = (kod, govde) => {
    yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
    yanit.end(JSON.stringify(govde));
  };

  if (adres.pathname === "/.well-known/openid-configuration") {
    return json(200, {
      issuer: YAYINCI, authorization_endpoint: `${YAYINCI}/yetkilendir`,
      token_endpoint: `${YAYINCI}/belirtec`, userinfo_endpoint: `${YAYINCI}/kullanici`,
      jwks_uri: `${YAYINCI}/anahtarlar`, id_token_signing_alg_values_supported: ["RS256"],
      scopes_supported: ["openid", "profile", "odunc:oku"], response_types_supported: ["code"],
    });
  }

  if (adres.pathname === "/anahtarlar") {
    return json(200, {
      keys: ANAHTARLAR.map((a) => ({
        ...a.publicKey.export({ format: "jwk" }), kid: a.kid, alg: "RS256", use: "sig",
      })),
    });
  }

  if (istek.method === "GET" && adres.pathname === "/yetkilendir") {
    if (s.get("client_id") !== ISTEMCI.kimlik || s.get("redirect_uri") !== ISTEMCI.yonlendirme) {
      return json(400, { error: "invalid_request" });
    }
    const kapsam = (s.get("scope") ?? "").split(" ");
    // Kimlik katmani yalnizca "openid" kapsami istendiginde devreye girer.
    if (!kapsam.includes("openid")) return json(400, { error: "invalid_scope", detay: "openid kapsami yok" });
    if (!s.get("state") || !s.get("nonce") || !s.get("code_challenge")) return json(400, { error: "invalid_request" });

    const kod = randomBytes(24).toString("base64url");
    KODLAR.set(kod, { meydan: s.get("code_challenge"), nonce: s.get("nonce"), kapsam, kullanildi: false });
    const u = new URL(ISTEMCI.yonlendirme);
    u.searchParams.set("code", kod); u.searchParams.set("state", s.get("state"));
    yanit.writeHead(302, { location: u.toString() });
    return yanit.end();
  }

  if (istek.method === "POST" && adres.pathname === "/belirtec") {
    const g = await formOku(istek);
    const k = KODLAR.get(g.code ?? "");
    if (!k || k.kullanildi) return json(400, { error: "invalid_grant" });
    if (!g.code_verifier || !esitMi(s256(g.code_verifier), k.meydan)) return json(400, { error: "invalid_grant" });
    k.kullanildi = true;

    const simdi = Math.floor(Date.now() / 1000);
    // Erisim belirteci kaynak sunucusu icin; kimlik belirteci istemci icin uretilir.
    const erisim = jwsUret(YURURLUKTE, { iss: YAYINCI, sub: KULLANICI.sub, aud: "odunc-servisi",
      scope: k.kapsam.join(" "), exp: simdi + 600, iat: simdi });
    const kimlik = jwsUret(YURURLUKTE, { iss: YAYINCI, sub: KULLANICI.sub, aud: ISTEMCI.kimlik,
      nonce: k.nonce, auth_time: simdi, exp: simdi + 600, iat: simdi,
      name: KULLANICI.ad, email: KULLANICI.eposta });
    return json(200, { access_token: erisim, id_token: kimlik, token_type: "Bearer", expires_in: 600 });
  }

  if (adres.pathname === "/kullanici") {
    // Kullanici ucu erisim belirteci ister; kimlik belirteci burada gecerli degildir.
    const b = istek.headers.authorization ?? "";
    if (!b.startsWith("Bearer ")) return json(401, { error: "invalid_token" });
    const iddialar = jwsDogrula(b.slice(7));
    if (!iddialar) return json(401, { error: "invalid_token" });
    if (iddialar.aud !== "odunc-servisi") return json(401, { error: "invalid_token", detay: "hedef kitle bu uc degil" });
    return json(200, KULLANICI);
  }
  json(404, { error: "not_found" });
}).listen(PORT, "127.0.0.1", () => console.log(`saglayici ${YAYINCI}`));
```

Anahtar kümesi iki anahtar yayımlar: yürürlükte olan ve emekliye ayrılmış olan. Anahtar
değişimi bu yüzden kesintisiz yapılabilir — yeni anahtar kümeye eklenir, imzalama ona
geçirilir, eski anahtar yürürlükteki belirteçlerin ömrü bitene kadar kümede kalır ve sonra
çıkarılır. Başlıktaki `kid` alanı doğrulayana hangi anahtarı seçeceğini söyler.

```bash
node saglayici.mjs 8561 > /dev/null 2>&1 & S=$!
sleep 1
curl -s http://127.0.0.1:8561/anahtarlar | sed 's/"n":"[^"]\{20\}[^"]*"/"n":"…"/g'
echo
kill $S
```

```
{"keys":[{"kty":"RSA","n":"…","e":"AQAB","kid":"2026-a","alg":"RS256","use":"sig"},{"kty":"RSA","n":"…","e":"AQAB","kid":"2025-b","alg":"RS256","use":"sig"}]}
```

Kümede yalnız açık anahtarlar vardır (`n` kısaltılmıştır). Doğrulayan taraf özel anahtarı hiç
görmez; belirteç üretme gücü sağlayıcıda kalır. JSON web belirteçleri dersinde paylaşılan
sırla imzalamanın yarattığı ölçek sorunu böyle çözülür.

#### İstemci ve Doğrulama Adımları

İstemci hiçbir adresi kendi içine yazmaz; keşif belgesinden okur. Belirteci aldıktan sonra
doğrulama, sırayla yanıtlanan sorulardan oluşur.

```js
// istemci.mjs — kesif belgesini okur, akisi yurutur, kimlik belirtecini adim adim dogrular
// Kullanim: node saglayici.mjs 8561 &  calisirken  node istemci.mjs
import { createHash, createPublicKey, randomBytes, verify } from "node:crypto";

const SAGLAYICI = "http://127.0.0.1:8561";
const ISTEMCI_KIMLIGI = "okuma-uygulamasi";
const YONLENDIRME = "http://127.0.0.1:8563/geri";
const s256 = (v) => createHash("sha256").update(v).digest("base64url");
const coz = (p) => JSON.parse(Buffer.from(p, "base64url").toString("utf8"));

// 1) Kesif belgesi: uc adresleri ve desteklenen algoritmalar buradan okunur.
const kesif = await (await fetch(`${SAGLAYICI}/.well-known/openid-configuration`)).json();
const anahtarKumesi = await (await fetch(kesif.jwks_uri)).json();
console.log("yayıncı           :", kesif.issuer);
console.log("imza algoritmaları:", kesif.id_token_signing_alg_values_supported.join(", "));
console.log("anahtar kümesi    :", anahtarKumesi.keys.map((a) => `${a.kid} (${a.kty})`).join(", "));

// 2) Akis: istemci state, nonce ve PKCE dogrulayicisini kendi uretir ve saklar.
const state = randomBytes(9).toString("base64url");
const nonce = randomBytes(9).toString("base64url");
const dogrulayici = randomBytes(32).toString("base64url");
const u = new URL(kesif.authorization_endpoint);
for (const [ad, deger] of Object.entries({ response_type: "code", client_id: ISTEMCI_KIMLIGI,
  redirect_uri: YONLENDIRME, scope: "openid profile odunc:oku", state, nonce,
  code_challenge: s256(dogrulayici), code_challenge_method: "S256" })) u.searchParams.set(ad, deger);

const yon = await fetch(u, { redirect: "manual" });
const geri = new URL(yon.headers.get("location"));
if (geri.searchParams.get("state") !== state) throw new Error("state eşleşmedi");

const belirtecler = await (await fetch(kesif.token_endpoint, {
  method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({ grant_type: "authorization_code", code: geri.searchParams.get("code"),
    redirect_uri: YONLENDIRME, client_id: ISTEMCI_KIMLIGI, code_verifier: dogrulayici }),
})).json();

// 3) Kimlik belirtecinin dogrulanmasi. Her adim ayri bir soruya karsilik gelir.
const kimlikDogrula = (belirtec, { beklenenNonce } = {}) => {
  const [b, g, i] = (belirtec ?? "").split(".");
  if (!b || !g || !i) return "biçim";
  const baslik = coz(b);
  if (!kesif.id_token_signing_alg_values_supported.includes(baslik.alg)) return "algoritma";
  const jwk = anahtarKumesi.keys.find((a) => a.kid === baslik.kid && a.alg === baslik.alg);
  if (!jwk) return "anahtar tanınmıyor";
  const acikAnahtar = createPublicKey({ key: jwk, format: "jwk" });
  if (!verify("sha256", Buffer.from(`${b}.${g}`), acikAnahtar, Buffer.from(i, "base64url"))) return "imza";

  const c = coz(g), simdi = Math.floor(Date.now() / 1000);
  if (c.iss !== kesif.issuer) return "yayıncı";
  const kitle = Array.isArray(c.aud) ? c.aud : [c.aud];
  if (!kitle.includes(ISTEMCI_KIMLIGI)) return `hedef kitle (${c.aud})`;
  if (simdi >= c.exp) return "süre";
  if (beklenenNonce !== undefined && c.nonce !== beklenenNonce) return "nonce";
  return { sub: c.sub, ad: c.name, kimlikDogrulamaAni: c.auth_time };
};

const yaz = (ad, sonuc) => console.log(ad.padEnd(30), typeof sonuc === "string" ? `ret (${sonuc})` : `kabul → ${JSON.stringify(sonuc)}`);
yaz("kimlik belirteci", kimlikDogrula(belirtecler.id_token, { beklenenNonce: nonce }));
yaz("beklenen nonce başka", kimlikDogrula(belirtecler.id_token, { beklenenNonce: "baska-deger" }));
yaz("erişim belirteci sunulunca", kimlikDogrula(belirtecler.access_token, { beklenenNonce: nonce }));

const [b, g, i] = belirtecler.id_token.split(".");
const degisikGovde = Buffer.from(JSON.stringify({ ...coz(g), sub: "Y-0002" })).toString("base64url");
yaz("gövdesi değiştirilmiş", kimlikDogrula(`${b}.${degisikGovde}.${i}`, { beklenenNonce: nonce }));
const baskaKid = Buffer.from(JSON.stringify({ ...coz(b), kid: "1999-z" })).toString("base64url");
yaz("tanınmayan anahtar kimliği", kimlikDogrula(`${baskaKid}.${g}.${i}`, { beklenenNonce: nonce }));

// 4) Kullanici ucu erisim belirteci ister.
const ucaSor = async (belirtec) => {
  const c = await fetch(kesif.userinfo_endpoint, { headers: { authorization: `Bearer ${belirtec}` } });
  return `${c.status} ${JSON.stringify(await c.json())}`;
};
console.log("kullanıcı ucu / erişim belirteci:", await ucaSor(belirtecler.access_token));
console.log("kullanıcı ucu / kimlik belirteci:", await ucaSor(belirtecler.id_token));
```

```bash
node saglayici.mjs 8561 > /dev/null 2>&1 & S=$!
sleep 1.2
node istemci.mjs
kill $S
```

```
yayıncı           : http://127.0.0.1:8561
imza algoritmaları: RS256
anahtar kümesi    : 2026-a (RSA), 2025-b (RSA)
kimlik belirteci               kabul → {"sub":"U-1001","ad":"Deniz Aral","kimlikDogrulamaAni":1785273591}
beklenen nonce başka           ret (nonce)
erişim belirteci sunulunca     ret (hedef kitle (odunc-servisi))
gövdesi değiştirilmiş          ret (imza)
tanınmayan anahtar kimliği     ret (anahtar tanınmıyor)
kullanıcı ucu / erişim belirteci: 200 {"sub":"U-1001","ad":"Deniz Aral","eposta":"deniz.aral@ornek.kutuphane","sube":"merkez"}
kullanıcı ucu / kimlik belirteci: 401 {"error":"invalid_token","detay":"hedef kitle bu uc degil"}
```

Zaman damgası her çalıştırmada değişir. Beş satır beş ayrı adımı doğruluyor.

**Algoritma**, keşif belgesinin bildirdiği listeyle sınırlanır ve belirtecin başlığından
alınmaz. JSON web belirteçleri dersinde kurulan kural burada da geçerlidir: kabul edilecek
algoritmalar doğrulayıcının yapılandırmasından gelir.

**Anahtar seçimi**, `kid` ile kümedeki anahtarlar arasında yapılır. Kümede karşılığı olmayan
bir `kid`, doğrulamayı başlamadan durdurur. Doğrulayıcının, tanımadığı bir `kid` için kümeyi
yeniden çekmesi olağandır; ancak bu yenileme sınırlanmalıdır, aksi hâlde her tanınmayan
değer bir dış çağrı doğurur.

**Hedef kitle**, kimlik belirteci ile erişim belirtecini ayıran adımdır. Üçüncü satırda erişim
belirteci kimlik belirteci gibi sunuluyor ve `aud` alanı `odunc-servisi` olduğu için
reddediliyor. Son satır aynı ayrımın ters yönünü gösteriyor: kimlik belirteci kullanıcı ucuna
sunulduğunda 401 alıyor. İki belirteç aynı yayıncıdan, aynı anahtarla imzalanmış olmasına
karşın birbirinin yerine geçmiyor; ayrımı yapan tek alan `aud` iddiasıdır.

**Nonce**, kimlik belirtecini istemcinin **bu** oturumuna bağlar. İstemci değeri üretir,
akışla birlikte gönderir ve dönen belirteçte aynısını arar. Eşleşmeyen bir belirteç, başka bir
akışa aitse tanınmaz. `state` isteğin istemciye ait olduğunu, `nonce` belirtecin bu isteğe ait
olduğunu doğrular; ikisi ayrı sorulardır ve ikisi de gereklidir.

**Kullanıcı ucu** (UserInfo endpoint), kimlik belirtecinde bulunmayan ek bilgiyi erişim
belirteciyle verir. Şube bilgisi bu uçta dönüyor ama kimlik belirtecinde yok; belirteci
büyütmek yerine bilgiyi ayrı bir çağrıya bırakmak, her istekte taşınan başlığı küçük tutar.

#### Kimlik Belirtecinin Sınırı

Kimlik belirteci geçmişe dair bir bildirimdir: `auth_time` anında bu kullanıcının kimliği
doğrulandı. Sürekli bir yetki değildir. İstemci belirteci doğruladıktan sonra kendi oturumunu
açar ve o oturumu kendi kurallarıyla yönetir — oturum tabanlı kimlik doğrulama dersindeki
bütün kararlar burada da geçerlidir, oturum kimliğinin girişte yenilenmesi dâhil.

Bundan çıkan kural şudur: kimlik belirteci her isteğe iliştirilmez, kaynak sunucusuna
gönderilmez ve uzun süre saklanmaz. Bir kez doğrulanır, karşılığı yerel oturuma yazılır ve
işi biter.

#### Özet

- Yetki devri protokolü erişim yetkisini devreder; kimlik katmanı buna kullanıcının kim
  olduğu bilgisini ekler ve `openid` kapsamıyla devreye girer.
- Kimlik belirtecinin hedef kitlesi istemci, erişim belirtecinin hedef kitlesi kaynak
  sunucusudur; ikisi aynı anahtarla imzalansa bile birbirinin yerine geçmez.
- Keşif belgesi uç adreslerini ve kabul edilen imza algoritmalarını bildirir; anahtar kümesi
  açık anahtarları `kid` ile yayımlar ve anahtar değişimini kesintisiz kılar.
- Doğrulama sırayla algoritma, anahtar seçimi, imza, yayıncı, hedef kitle, süre ve nonce
  adımlarından geçer; her adım ayrı bir soruyu yanıtlar.
- `state` isteğin istemciye, `nonce` belirtecin o isteğe ait olduğunu doğrular; ikisi birbirinin
  yerine kullanılamaz.
- Kimlik belirteci geçmişteki bir doğrulamanın bildirimidir; bir kez doğrulanır, karşılığı
  yerel oturuma yazılır ve her isteğe iliştirilmez.

#### Sonraki Adım

Bu kurulumda kimlik sağlayıcı ile ödünç servisi aynı kurumun parçasıydı: sağlayıcı üye
kaydını biliyor, `sub` alanı doğrudan üye koduna karşılık geliyordu. İlçe kütüphaneleri ortak
bir ödünç sistemine bağlandığında bu varsayım düşer — her kurumun kendi kullanıcı dizini,
kendi kimlik doğrulama kuralları ve kendi kullanıcı kimlikleri vardır. Sonraki ders bu
düzeni kurar: bir kurumun doğruladığı kimliğin, imzalı bir **iddia belgesiyle** başka bir
kuruma taşınması, bu belgenin geçerlilik penceresi ve hedef kitlesiyle denetlenmesi, aynı
belgenin ikinci kez kullanılmasının engellenmesi ve dışarıdan gelen özniteliklerin yerel
role çevrilmesi.

### Kurumsal Federasyon

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/kurumsal-federasyon

İddia belgesiyle taşınan kimliğin imza, yayıncı, hedef kitle ve süre penceresiyle denetlenmesi, tekrar kullanımın engellenmesi, dışarıdan gelen özniteliklerin açık bir tabloyla yerel role çevrilmesi ve çıkışın sınırı.

Önceki ders kimlik katmanını yetki devri protokolünün üstüne ekledi: uygulama, kullanıcının
kim olduğunu imzalı bir belgeden öğrendi. Belgeyi üreten taraf ile tüketen taraf aynı
kurumun parçalarıydı.

Kurumsal federasyonda bu varsayım kalkar. Kullanıcı kimliğini **kendi kurumunda**
doğrular; kütüphane servisi o kurumun ürettiği belgeye güvenerek kullanıcıyı kabul eder.
Kütüphane parolayı hiç görmez, kullanıcı listesini hiç tutmaz. Bu düzenin adı **çoklu
oturum açma (single sign-on)**, taşıyıcısı ise **iddia belgesidir (assertion)**.

#### Güvenin Yönü ve Sınırı

Federasyonda iki taraf vardır: kimliği doğrulayan **kimlik sağlayıcı** ve kaynağa erişimi
yöneten **hizmet sağlayıcı**. Güven tek yönlüdür ve sınırlıdır: hizmet sağlayıcı, kimlik
sağlayıcının "bu kişi budur" iddiasına güvenir; "bu kişi şunu yapabilir" kararını kendisi
verir.

Bu ayrım kursun ilk dersindeki ayrımın kurumlar arası biçimidir. Kimlik dışarıdan gelir,
yetki içeride kalır. Dışarıdan gelen bir öznitelik doğrudan yetkiye çevrilirse, komşu
kurumun grup yönetimi bizim yetki sistemimizi belirler hâle gelir.

Aşağıdaki hesap bir iddia belgesinin üretilmesini, doğrulanmasını ve yerel role
çevrilmesini uçtan uca çalıştırır.

```bash
cat > federasyon.mjs <<'EOF'
// federasyon.mjs — iddia belgesinin uretilmesi, dogrulanmasi ve yerel role cevrilmesi
import { createHmac, timingSafeEqual, randomBytes } from "node:crypto";

const SIR = Buffer.from("kurum-imzalama-anahtari-ornek");
const b64 = (o) => Buffer.from(JSON.stringify(o)).toString("base64url");
const imzala = (govde) => createHmac("sha256", SIR).update(govde).digest("base64url");

// Kimlik saglayan kurum bir iddia belgesi uretir.
function iddiaUret({ ozne, epostaDogrulandi, gruplar, hedef, saniye, saat }) {
  const govde = b64({
    iss: "https://kimlik.kuzey-universite.test",
    sub: ozne, aud: hedef,
    iat: saat, exp: saat + saniye,
    jti: randomBytes(6).toString("hex"),
    eposta_dogrulandi: epostaDogrulandi, gruplar,
  });
  return govde + "." + imzala(govde);
}

// Hizmet saglayan kurum belgeyi dogrular. Her adim ayri bir karar.
const KULLANILAN = new Set();
function dogrula(belge, { hedef, saat, yayinci }) {
  const [govde, imza] = belge.split(".");
  const beklenen = imzala(govde);
  if (imza.length !== beklenen.length ||
      !timingSafeEqual(Buffer.from(imza), Buffer.from(beklenen))) return "red: imza";
  const i = JSON.parse(Buffer.from(govde, "base64url"));
  if (i.iss !== yayinci) return "red: yayinci";
  if (i.aud !== hedef) return "red: hedef kitle";
  if (saat >= i.exp) return "red: suresi gecmis";
  if (saat < i.iat) return "red: gelecek tarihli";
  if (KULLANILAN.has(i.jti)) return "red: yeniden kullanim";
  KULLANILAN.add(i.jti);
  return "kabul";
}

const SAAT = 1_770_000_000;                       // sabit saat: cikti yeniden uretilebilsin
const HEDEF = "https://kutuphane.kuzey-universite.test";
const YAYINCI = "https://kimlik.kuzey-universite.test";
const temel = { ozne: "ogr-4417", epostaDogrulandi: true, gruplar: ["ogrenci", "fen-fakultesi"],
                hedef: HEDEF, saniye: 300, saat: SAAT };

const denemeler = [
  ["gecerli belge", iddiaUret(temel), { hedef: HEDEF, saat: SAAT + 10, yayinci: YAYINCI }],
  ["baska kuruma yazilmis", iddiaUret({ ...temel, hedef: "https://baska.test" }), { hedef: HEDEF, saat: SAAT + 10, yayinci: YAYINCI }],
  ["suresi gecmis", iddiaUret({ ...temel, saniye: 5 }), { hedef: HEDEF, saat: SAAT + 60, yayinci: YAYINCI }],
  ["imzasi bozulmus", iddiaUret(temel).slice(0, -1) + "x", { hedef: HEDEF, saat: SAAT + 10, yayinci: YAYINCI }],
  ["baska yayincidan", iddiaUret(temel), { hedef: HEDEF, saat: SAAT + 10, yayinci: "https://sahte.test" }],
];
console.log("deneme                    sonuc");
for (const [ad, belge, kosul] of denemeler) console.log(`  ${ad.padEnd(24)} ${dogrula(belge, kosul)}`);

// Ayni belge iki kez sunulursa
const tekrar = iddiaUret(temel);
console.log(`  ${"ilk sunum".padEnd(24)} ${dogrula(tekrar, { hedef: HEDEF, saat: SAAT + 10, yayinci: YAYINCI })}`);
console.log(`  ${"ayni belge ikinci kez".padEnd(24)} ${dogrula(tekrar, { hedef: HEDEF, saat: SAAT + 20, yayinci: YAYINCI })}`);

// Disaridan gelen oznitelikler yerel role cevrilir; esleme tablosu acik olmali.
const ESLEME = [
  { kosul: (i) => i.gruplar.includes("kutuphane-personeli"), rol: "gorevli" },
  { kosul: (i) => i.gruplar.includes("ogrenci") && i.eposta_dogrulandi, rol: "uye" },
  { kosul: (i) => i.gruplar.includes("akademik"), rol: "uye" },
];
function yerelRol(belge) {
  const i = JSON.parse(Buffer.from(belge.split(".")[0], "base64url"));
  const eslesen = ESLEME.find((e) => e.kosul(i));
  return { ozne: i.sub, gruplar: i.gruplar.join(","), rol: eslesen ? eslesen.rol : "rol yok" };
}
console.log("\ndisaridan gelen ozne       gruplar                     yerel rol");
for (const g of [["ogrenci", "fen-fakultesi"], ["kutuphane-personeli"], ["misafir"]]) {
  const r = yerelRol(iddiaUret({ ...temel, gruplar: g }));
  console.log(`  ${r.ozne.padEnd(24)} ${r.gruplar.padEnd(27)} ${r.rol}`);
}
const dogrulanmamis = yerelRol(iddiaUret({ ...temel, epostaDogrulandi: false, gruplar: ["ogrenci"] }));
console.log(`  ${dogrulanmamis.ozne.padEnd(24)} ${"ogrenci (eposta dogrulanmamis)".padEnd(27)} ${dogrulanmamis.rol}`);
EOF
node federasyon.mjs
```

```text
deneme                    sonuc
  gecerli belge            kabul
  baska kuruma yazilmis    red: hedef kitle
  suresi gecmis            red: suresi gecmis
  imzasi bozulmus          red: imza
  baska yayincidan         red: yayinci
  ilk sunum                kabul
  ayni belge ikinci kez    red: yeniden kullanim

disaridan gelen ozne       gruplar                     yerel rol
  ogr-4417                 ogrenci,fen-fakultesi       uye
  ogr-4417                 kutuphane-personeli         gorevli
  ogr-4417                 misafir                     rol yok
  ogr-4417                 ogrenci (eposta dogrulanmamis) rol yok
```

#### Doğrulama Tek Adım Değildir

Çıktının ilk bölümü beş ayrı reddi gösteriyor ve her biri ayrı bir denetimden geliyor.
İmza, belgenin yolda değişmediğini söyler — ama tek başına yeterli değildir. Yayıncı
denetimi, belgenin **beklenen kurumdan** geldiğini doğrular; bu denetim atlanırsa, geçerli
imzalı bir belge üretebilen herhangi bir kurum bizim kullanıcılarımızı tanımlayabilir.

Hedef kitle denetimi en sık atlananıdır. Kimlik sağlayıcı aynı kullanıcı için birden çok
hizmete belge üretir; bir hizmet için üretilmiş belge diğerinde kabul edilirse, zayıf
korunan bir servis güçlü korunan bir servise geçiş kapısı olur. Belgenin hedefi, onu kimin
kabul edeceğini yazar.

Süre penceresi iki yönlüdür: geçmiş taraf belgenin ömrünü, gelecek taraf saat kaymasını
denetler. Pencerenin dar tutulması, belge ele geçirildiğinde kullanım süresini kısaltır.

#### Aynı Belge İkinci Kez Kullanılamaz

İkinci bölümdeki iki satır tekrar kullanım savunmasını gösteriyor. Belge, benzersiz bir
tanımlayıcı taşır ve tüketildiğinde bu tanımlayıcı kaydedilir; aynı belge ikinci kez
sunulduğunda reddedilir.

Kaydın ömrü belgenin ömrü kadardır: süresi geçmiş bir belgenin tanımlayıcısını saklamak
gereksizdir, çünkü süre denetimi zaten reddeder. Bu, kaydın sınırsız büyümesini engeller —
tekrar kullanım listesi, geçerlilik penceresi kadar geriye bakan bir kayıttır.

#### Öznitelikten Role Çevrim Açık Olmalı

Üçüncü bölüm eşleme tablosunu çalıştırıyor. Dışarıdan gelen grup adları doğrudan rol olarak
kullanılmıyor; yerel bir tabloya göre çevriliyor. Eşleşmeyen bir grup "rol yok" sonucunu
veriyor ve kullanıcı kaynağa erişemiyor.

Son satır ek bir koşulu gösteriyor: öğrenci grubunda olmak yetmiyor, e-postanın doğrulanmış
olması da gerekiyor. Kimlik sağlayıcının gönderdiği her öznitelik aynı güvenilirlikte
değildir; hangi özniteliğe hangi koşulla güvenileceği hizmet sağlayıcının kararıdır.

Tablonun açık olmasının pratik sonucu şudur: komşu kurum yeni bir grup açtığında bizim
sistemimizde hiçbir şey değişmez. Yetki genişlemesi ancak eşleme tablosuna bir satır
eklendiğinde olur ve o satır incelemeden geçer.

#### Çıkışın Sınırı

Federasyonda çıkış işlemi tek taraflı değildir. Kullanıcı kütüphane servisinden çıktığında
kimlik sağlayıcıdaki oturumu sürer; sonraki istekte yeniden yönlendirme sessizce yeni bir
belge üretir ve kullanıcı hiç çıkmamış gibi görünür.

Bunun iki karşılığı vardır. Yerel çıkış, yalnız hizmet sağlayıcıdaki oturumu sonlandırır ve
"başka bir hesapla gir" isteğini karşılamaz. Merkezî çıkış, kimlik sağlayıcıdaki oturumu da
sonlandırır ve o sağlayıcıya bağlı bütün hizmetleri etkiler. Ortak makinelerde ikincisi
gerekir; kullanıcıya hangisinin yapıldığı açıkça söylenmelidir.

#### Özet

- Federasyonda kimlik dışarıdan gelir, yetki içeride kalır: iddia belgesi kişiyi bildirir,
  ne yapabileceğini hizmet sağlayıcı belirler.
- Doğrulama tek adım değildir; imza, yayıncı, hedef kitle ve süre penceresi ayrı ayrı
  denetlenir — modelde bu denetimlerin her biri ayrı bir reddi üretti.
- Belge benzersiz tanımlayıcısıyla bir kez tüketilir; tekrar kullanım listesi belgenin
  geçerlilik penceresi kadar geriye bakar.
- Dışarıdan gelen öznitelikler açık bir tabloyla yerel role çevrilir; eşleşmeyen grup yetki
  almaz ve komşu kurumun grup açması bizim yetkilerimizi değiştirmez.
- Yerel çıkış ile merkezî çıkış farklı şeylerdir; hangisinin yapıldığı kullanıcıya
  bildirilmelidir.

#### Sonraki Adım

Buraya kadarki bütün akışlarda karşıda bir insan vardı: parola giren, onay ekranını
okuyan, ikinci etmeni sağlayan biri. Kütüphane servisini çağıranların bir bölümü ise
insan değildir — gece çalışan bir toplu iş, komşu kurumun katalog eşitleyicisi, bir
gösterge panosu. Bunların ne parolası ne de onay ekranı vardır. Sonraki ders bu çağıranlar
için kullanılan kimlik bilgisini ele alır: nasıl üretilir, nerede saklanır ve nasıl
döndürülür?

### API Anahtarları

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/api-anahtarlari

Makine çağıranlar için üretilen uzun ömürlü kimlik bilgisinin entropisi, önek ve son karakterlerin işletim değeri, karma olarak saklanması, geçiş penceresiyle döndürülmesi ve kapsamla sınırlanması.

Buraya kadarki bütün akışlarda karşıda bir insan vardı: parola giren, onay ekranını okuyan,
ikinci etmeni sağlayan biri. Kütüphane servisini çağıranların bir bölümü ise insan değildir.
Gece çalışan bir toplu iş, komşu kurumun katalog eşitleyicisi, bir gösterge panosu — bunların
ne parolası vardır ne de bir onay ekranını okuyabilirler.

Bu çağıranlar için kullanılan kimlik bilgisine **API anahtarı** denir. Yapısı yalındır:
uzun, rastgele bir dizgi. Yalınlığı hem gücü hem zayıflığıdır — taşınması kolaydır,
sızması da kolaydır.

#### Anahtar Neyi Kanıtlar

API anahtarı **kim olduğunu** kanıtlar, ne yapabileceğini değil. Bu ayrım kursun ilk
dersindeki ayrımın makineler için geçerli biçimidir: anahtar doğrulanır, yetki ayrıca
sınırlanır.

Anahtarın belirteçten farkı ömrüdür. Belirteç kısa yaşar ve yenilenir; anahtar aylarca
aynı kalır ve yapılandırma dosyalarında, sürekli tümleştirme ortamlarında, kap
görüntülerinde durur. Bu yüzden anahtarın tasarımında iki soru öne çıkar: sızdığında ne
kadar zarar verir ve ne kadar hızlı değiştirilebilir?

Aşağıdaki hesap anahtarın üretiminden döndürülmesine kadar bütün yaşam döngüsünü
çalıştırır.

```bash
cat > anahtar.mjs <<'EOF'
// anahtar.mjs — API anahtarinin uretimi, saklanmasi, dogrulanmasi ve dondurulmesi
import { randomBytes, createHash, timingSafeEqual } from "node:crypto";
import { DatabaseSync } from "node:sqlite";

// 1. Uretim: onek + rastgele govde. Onek anahtarin nereye ait oldugunu okunur kilar.
const ONEK = "kutup_";
function anahtarUret(bayt) {
  const govde = randomBytes(bayt).toString("base64url");
  return ONEK + govde;
}
const entropi = (bayt) => bayt * 8;
console.log("govde bayt  entropi (bit)  ornek uzunluk  kaba kuvvet icin gereken deneme (2^n)");
for (const b of [8, 16, 24, 32]) {
  console.log(`${String(b).padStart(10)} ${String(entropi(b)).padStart(14)} ${String(anahtarUret(b).length).padStart(14)}  2^${entropi(b) - 1}`);
}

// 2. Saklama: anahtarin kendisi degil karmasi tutulur; onek ve son dort karakter aranabilir.
const db = new DatabaseSync(":memory:");
db.exec(`CREATE TABLE api_anahtari (
  kimlik TEXT PRIMARY KEY, sahip TEXT NOT NULL, karma TEXT NOT NULL,
  kuyruk TEXT NOT NULL, durum TEXT NOT NULL, gecerlilik_sonu INTEGER)`);
const karmala = (a) => createHash("sha256").update(a).digest("hex");
function kaydet(kimlik, sahip, anahtar, gecerlilikSonu = null) {
  db.prepare("INSERT INTO api_anahtari VALUES (?,?,?,?,?,?)")
    .run(kimlik, sahip, karmala(anahtar), anahtar.slice(-4), "etkin", gecerlilikSonu);
}
const eski = anahtarUret(24);
kaydet("AK-1", "katalog-esitleyici", eski);
// Karma ve kuyruk her uretimde farklidir; burada yalniz uzunluklari yazdirilir.
console.log("\nsaklanan kayit (anahtarin kendisi yok):");
for (const s of db.prepare("SELECT kimlik, sahip, length(karma) AS karma_uzunluk, length(kuyruk) AS kuyruk_uzunluk, durum FROM api_anahtari").all())
  console.log("  " + JSON.stringify(s));

// 3. Dogrulama: sabit sureli karsilastirma; eslesme yoksa neden acikca yazilmaz.
const SAAT = 1_770_000_000;
function dogrula(sunulan, saat) {
  const k = karmala(sunulan);
  const satir = db.prepare("SELECT * FROM api_anahtari WHERE kuyruk = ?").all(sunulan.slice(-4))
    .find((s) => {
      const a = Buffer.from(s.karma, "hex"), b = Buffer.from(k, "hex");
      return a.length === b.length && timingSafeEqual(a, b);
    });
  if (!satir) return "red: gecersiz anahtar";
  if (satir.durum !== "etkin") return `red: anahtar ${satir.durum}`;
  if (satir.gecerlilik_sonu !== null && saat >= satir.gecerlilik_sonu) return "red: suresi gecmis";
  return `kabul (${satir.sahip})`;
}
console.log("\ndogrulama denemeleri");
console.log("  gecerli anahtar          -> " + dogrula(eski, SAAT));
console.log("  rastgele baska anahtar   -> " + dogrula(anahtarUret(24), SAAT));

// 4. Dondurme: yeni anahtar uretilir, eski bir gecis penceresi boyunca gecerli kalir.
const yeni = anahtarUret(24);
kaydet("AK-2", "katalog-esitleyici", yeni);
db.prepare("UPDATE api_anahtari SET gecerlilik_sonu = ? WHERE kimlik = 'AK-1'").run(SAAT + 3600);
const asamalar = [
  ["dondurmeden once", SAAT - 10],
  ["gecis penceresinde", SAAT + 60],
  ["pencere kapandiktan sonra", SAAT + 7200],
];
console.log("\nasama                      eski anahtar            yeni anahtar");
for (const [ad, saat] of asamalar)
  console.log(`  ${ad.padEnd(26)} ${dogrula(eski, saat).padEnd(23)} ${dogrula(yeni, saat)}`);

// 5. Sizinti sonrasi: iptal aninda etkili olur, sureye bakilmaz.
db.prepare("UPDATE api_anahtari SET durum = 'iptal' WHERE kimlik = 'AK-2'").run();
console.log("\niptalden sonra yeni anahtar -> " + dogrula(yeni, SAAT + 60));

// 6. Kapsam ve kaynak sinirlamasi: anahtar tek basina yetki degildir.
const SINIR = { "AK-1": { kapsamlar: ["katalog:oku"], agBloklari: ["10.0.0.0/8"] } };
console.log("\nanahtar basina sinir: " + JSON.stringify(SINIR["AK-1"]));
console.log("  anahtar dogrulanir, yetki ayrica kapsamla ve kaynak agiyla sinirlanir.");
EOF
node anahtar.mjs
```

```text
govde bayt  entropi (bit)  ornek uzunluk  kaba kuvvet icin gereken deneme (2^n)
         8             64             17  2^63
        16            128             28  2^127
        24            192             38  2^191
        32            256             49  2^255

saklanan kayit (anahtarin kendisi yok):
  {"kimlik":"AK-1","sahip":"katalog-esitleyici","karma_uzunluk":64,"kuyruk_uzunluk":4,"durum":"etkin"}

dogrulama denemeleri
  gecerli anahtar          -> kabul (katalog-esitleyici)
  rastgele baska anahtar   -> red: gecersiz anahtar

asama                      eski anahtar            yeni anahtar
  dondurmeden once           kabul (katalog-esitleyici) kabul (katalog-esitleyici)
  gecis penceresinde         kabul (katalog-esitleyici) kabul (katalog-esitleyici)
  pencere kapandiktan sonra  red: suresi gecmis      kabul (katalog-esitleyici)

iptalden sonra yeni anahtar -> red: anahtar iptal

anahtar basina sinir: {"kapsamlar":["katalog:oku"],"agBloklari":["10.0.0.0/8"]}
  anahtar dogrulanir, yetki ayrica kapsamla ve kaynak agiyla sinirlanir.
```

#### Üretim: Entropi ve Önek

Çıktının ilk tablosu gövde uzunluğunun entropiye çevrimini gösteriyor. On altı baytlık bir
gövde yüz yirmi sekiz bit entropi taşır; kaba kuvvetle bulunması pratikte olanaksızdır. Daha
uzun anahtarlar ek güvenlik getirmez, yalnız yapılandırma dosyalarını şişirir.

Önek ise güvenlik değil **işletim** içindir. Anahtar bir günlüğe ya da kaynak koduna
sızdığında, önek onun hangi servise ait olduğunu tek bakışta söyler; sızıntı taramaları da
bu önekle çalışır. Aynı nedenle anahtarın son birkaç karakteri kayıtta saklanır: kullanıcı
"hangi anahtarı iptal edeyim" sorusunu ancak böyle yanıtlayabilir.

#### Saklama: Anahtarın Kendisi Tutulmaz

İkinci bölüm saklanan kaydı gösteriyor ve kayıtta anahtarın kendisi yok. Tutulan şey
karması, arama için son dört karakteri ve durum bilgisidir.

Bunun sonucu, anahtarın **yalnız bir kez** gösterilebilmesidir: üretildiği anda kullanıcıya
verilir, sonra bir daha okunamaz. Kaybedilen anahtar kurtarılmaz, yenisi üretilir. Bu
kısıt bir kolaylık kaybı gibi görünür ama veritabanı sızıntısında anahtarların doğrudan
kullanılabilir olmasını engeller.

Parola saklamadan bir fark vardır: API anahtarı yüksek entropili ve rastgeledir, sözlük
saldırısına açık değildir. Bu yüzden yavaş karma gerekmez; hızlı bir karma yeterlidir ve
her istekte doğrulanacağı için tercih edilir. Doğrulamanın **sabit süreli** karşılaştırmayla
yapılması ise yine gereklidir.

#### Döndürme: Geçiş Penceresi

Dördüncü bölüm döndürmeyi aşama aşama gösteriyor. Yeni anahtar üretilir ve eski anahtara
bir son tarih verilir; geçiş penceresinde **ikisi de** geçerlidir. Bu, çağıran tarafın
yapılandırmasını güncellemesi için zaman tanır.

Pencere olmadan döndürme bir kesintidir: anahtar değiştiği anda eski yapılandırmayla çalışan
her iş düşer. Pencerenin uzunluğu, çağıran tarafın dağıtım sıklığına göre seçilir ve
pencerenin sonunda eski anahtarın gerçekten kullanılmadığı **ölçülerek** doğrulanır — son
kullanım zamanı kayıtta tutulur.

Beşinci bölüm istisnayı gösteriyor: sızıntı durumunda pencere yoktur. İptal anında etkili
olur ve süre denetiminden önce çalışır; kesinti, sızmış bir anahtarın açık kalmasına yeğdir.

#### Anahtar Tek Başına Yetki Değildir

Son bölüm anahtar başına sınırları listeliyor. Doğrulanmış bir anahtar, çağıranın kim
olduğunu söyler; ne yapabileceği kapsamla, nereden çağırabileceği kaynak ağı kısıtıyla
sınırlanır. Salt okuma yapan bir eşitleyicinin anahtarı yazma uç noktalarına erişememelidir
— bu, Kapsamlar ve İzinler dersindeki kuralın makine çağıranlar için uygulanmasıdır.

Anahtarın nerede saklandığı da tasarımın parçasıdır. Kaynak koduna, kap görüntüsüne ya da
sürüm kontrolüne yazılan anahtar, o depoya erişebilen herkese açıktır. Doğru yer,
çalışma zamanında okunan bir sır deposudur; Gizli Değerlerin Ayrılması dersindeki sınır
burada da geçerlidir.

#### Özet

- API anahtarı, onay ekranı olmayan makine çağıranlar için kullanılan uzun ömürlü bir
  kimlik bilgisidir; kim olduğunu kanıtlar, yetkiyi kapsam belirler.
- Önek ve son karakterler işletim içindir: sızıntı taraması ve "hangi anahtarı iptal
  edeyim" sorusu bunlarla çalışır.
- Kayıtta anahtarın kendisi değil karması tutulur; anahtar yalnız üretildiği anda
  gösterilir. Yüksek entropili olduğu için hızlı karma yeterlidir, karşılaştırma sabit
  sürelidir.
- Döndürmede eski ve yeni anahtar bir geçiş penceresinde birlikte geçerlidir; sızıntıda
  pencere yoktur, iptal anında etkilidir.
- Anahtar sır deposunda durur; kaynak koduna, kap görüntüsüne ya da sürüm kontrolüne
  yazılmaz.

#### Sonraki Adım

Anahtar, makine çağıranın tek kanıtıdır: onu elinde tutan çağırandır. İnsan kullanıcılarda
ise tek kanıta dayanmak yetersizdir — parola ele geçtiğinde hesabın tamamı açılır. Kursun bu
konudaki son dersi ikinci bir kanıt katmanını ele alıyor: hangi etmen türleri vardır, zaman
tabanlı kodun doğrulaması nasıl yapılır, saat kayması nasıl karşılanır ve kurtarma kodları
neden tek kullanımlıktır?

### Çok Etmenli Doğrulama

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/cok-etmenli-dogrulama

Etmen sınıflarının bağımsızlığı, zaman tabanlı kodun türetilmesi ve saat kayması penceresinin bedeli, aynı kodun ikinci kez kullanılmasının engellenmesi ve kurtarma kodlarının tek kullanımlık saklanması.

Önceki ders makine çağıranların tek kanıtını ele aldı: anahtarı elinde tutan çağırandır.
İnsan kullanıcılarda tek kanıta dayanmak yetersizdir. Parola bir kez ele geçtiğinde —
yeniden kullanılmış, oltalama sayfasına yazılmış ya da başka bir sızıntıdan gelmiş olabilir —
hesabın tamamı açılır.

**Çok etmenli doğrulama**, girişi birden çok bağımsız kanıta bağlar. Kazancın kaynağı
sayı değil **bağımsızlıktır**: iki kanıt aynı yoldan ele geçirilebiliyorsa ikinci kanıt bir
şey eklemez.

#### Etmen Sınıfları

Kanıtlar üç sınıfa ayrılır: bilinen bir şey (parola, kurtarma kodu), sahip olunan bir şey
(telefondaki uygulama, donanım anahtarı) ve kişinin kendisine ait bir şey (biyometrik).
Çok etmenli doğrulama, **farklı sınıflardan** en az iki kanıt ister.

Aşağıdaki hesap zaman tabanlı kodu üretir, doğrular ve etmen sınıflarını karşılaştırır.

```bash
cat > mfa.mjs <<'EOF'
// mfa.mjs — zaman tabanli tek kullanimlik kod, saat kaymasi ve kurtarma kodlari
import { createHmac, createHash, timingSafeEqual } from "node:crypto";

// 1. Zaman tabanli kod (TOTP): paylasilan sirdan ve zaman diliminden turetilir.
const ADIM = 30;                       // saniye
const BASAMAK = 6;
function totp(sir, saat, adim = ADIM) {
  const sayac = Math.floor(saat / adim);
  const tampon = Buffer.alloc(8);
  tampon.writeBigUInt64BE(BigInt(sayac));
  const ozet = createHmac("sha1", sir).update(tampon).digest();
  const kaydir = ozet[ozet.length - 1] & 0x0f;
  const deger = ozet.readUInt32BE(kaydir) & 0x7fffffff;
  return String(deger % 10 ** BASAMAK).padStart(BASAMAK, "0");
}

const SIR = Buffer.from("paylasilan-ornek-sir");
const SAAT = 1_770_000_000;
console.log("zaman           dilim       kod");
for (const fark of [0, 29, 30, 60]) {
  const t = SAAT + fark;
  console.log(`  +${String(fark).padEnd(3)} saniye    ${String(Math.floor(t / ADIM)).padEnd(11)} ${totp(SIR, t)}`);
}

// 2. Saat kaymasi penceresi: sunucu kac dilim geriye/ileriye bakmali?
function dogrula(sir, sunulan, saat, pencere) {
  for (let k = -pencere; k <= pencere; k++) {
    const beklenen = totp(sir, saat + k * ADIM);
    const a = Buffer.from(sunulan), b = Buffer.from(beklenen);
    if (a.length === b.length && timingSafeEqual(a, b)) return { kabul: true, dilimFarki: k };
  }
  return { kabul: false };
}
const kullaniciSaati = SAAT - 45;                 // kullanicinin saati 45 saniye geride
const kod = totp(SIR, kullaniciSaati);
console.log("\npencere  sonuc     dilim farki   deneme sayisi");
for (const p of [0, 1, 2]) {
  const s = dogrula(SIR, kod, SAAT, p);
  console.log(`${String(p).padStart(7)}  ${(s.kabul ? "kabul" : "red").padEnd(9)} ${String(s.kabul ? s.dilimFarki : "-").padEnd(13)} ${2 * p + 1}`);
}
console.log("pencere buyudukce kabul edilen kod sayisi artar: her ek dilim tahmin alanini genisletir.");

// 3. Ayni kodun ikinci kez kullanilmasi engellenir.
const KULLANILAN = new Set();
function tekKullanim(ozne, sunulan, saat) {
  const s = dogrula(SIR, sunulan, saat, 1);
  if (!s.kabul) return "red: kod gecersiz";
  const anahtar = `${ozne}:${Math.floor((saat + s.dilimFarki * ADIM) / ADIM)}`;
  if (KULLANILAN.has(anahtar)) return "red: kod zaten kullanildi";
  KULLANILAN.add(anahtar);
  return "kabul";
}
const guncelKod = totp(SIR, SAAT);
console.log("\ntek kullanim denemesi");
console.log("  ilk sunum    -> " + tekKullanim("U-1001", guncelKod, SAAT));
console.log("  ikinci sunum -> " + tekKullanim("U-1001", guncelKod, SAAT + 5));

// 4. Etmen turleri ve kurtarma: her etmen ayni sinifa dusmemeli.
const ETMENLER = [
  { ad: "parola",                   sinif: "bilgi",  cevrimdisi: true,  devredilebilir: true },
  { ad: "zaman tabanli kod",        sinif: "sahiplik", cevrimdisi: true,  devredilebilir: false },
  { ad: "kisa mesajla kod",         sinif: "sahiplik", cevrimdisi: false, devredilebilir: false },
  { ad: "donanim guvenlik anahtari", sinif: "sahiplik", cevrimdisi: true, devredilebilir: false },
  { ad: "kurtarma kodu",            sinif: "bilgi",  cevrimdisi: true,  devredilebilir: true },
];
console.log("\netmen                       sinif      cevrimdisi  devredilebilir");
for (const e of ETMENLER)
  console.log(`  ${e.ad.padEnd(26)} ${e.sinif.padEnd(10)} ${(e.cevrimdisi ? "evet" : "hayir").padEnd(11)} ${e.devredilebilir ? "evet" : "hayir"}`);
const ayniSinif = ETMENLER.filter((e) => e.sinif === "bilgi").length;
console.log(`bilgi sinifindaki etmen: ${ayniSinif} — ikinci etmen birincinin sinifindan secilirse kazanc yoktur.`);

// 5. Kurtarma kodlari: tek kullanimlik ve karma olarak saklanir.
const kurtarma = ["3f8a-91cd", "77b2-4e0a", "c15d-8823"];
const depo = new Map(kurtarma.map((k) => [createHash("sha256").update(k).digest("hex"), "kullanilmadi"]));
function kurtarmaDene(k) {
  const h = createHash("sha256").update(k).digest("hex");
  if (!depo.has(h)) return "red: gecersiz kod";
  if (depo.get(h) === "kullanildi") return "red: kod daha once kullanilmis";
  depo.set(h, "kullanildi");
  return "kabul";
}
console.log("\nkurtarma kodu denemeleri");
for (const [ad, k] of [["gecerli kod", kurtarma[0]], ["ayni kod tekrar", kurtarma[0]], ["listede olmayan", "0000-0000"]])
  console.log(`  ${ad.padEnd(18)} -> ${kurtarmaDene(k)}`);
console.log(`kalan kullanilabilir kod: ${[...depo.values()].filter((d) => d === "kullanilmadi").length} / ${kurtarma.length}`);
EOF
node mfa.mjs
```

```text
zaman           dilim       kod
  +0   saniye    59000000    942367
  +29  saniye    59000000    942367
  +30  saniye    59000001    325366
  +60  saniye    59000002    081076

pencere  sonuc     dilim farki   deneme sayisi
      0  red       -             1
      1  red       -             3
      2  kabul     -2            5
pencere buyudukce kabul edilen kod sayisi artar: her ek dilim tahmin alanini genisletir.

tek kullanim denemesi
  ilk sunum    -> kabul
  ikinci sunum -> red: kod zaten kullanildi

etmen                       sinif      cevrimdisi  devredilebilir
  parola                     bilgi      evet        evet
  zaman tabanli kod          sahiplik   evet        hayir
  kisa mesajla kod           sahiplik   hayir       hayir
  donanim guvenlik anahtari  sahiplik   evet        hayir
  kurtarma kodu              bilgi      evet        evet
bilgi sinifindaki etmen: 2 — ikinci etmen birincinin sinifindan secilirse kazanc yoktur.

kurtarma kodu denemeleri
  gecerli kod        -> kabul
  ayni kod tekrar    -> red: kod daha once kullanilmis
  listede olmayan    -> red: gecersiz kod
kalan kullanilabilir kod: 2 / 3
```

#### Zaman Tabanlı Kod Nasıl Doğrulanır

Çıktının ilk tablosu kodun zaman dilimine bağlı olduğunu gösteriyor: aynı dilim içindeki
her an aynı kodu verir, dilim değiştiğinde kod değişir. Kodu üreten şey paylaşılan sır ile
dilim numarasıdır; sunucu ile istemci arasında ağ üzerinden bir şey taşınmaz.

İkinci tablo saat kaymasını ölçüyor. Kullanıcının saati kırk beş saniye geride olduğunda
sıfır pencereyle doğrulama başarısız oluyor; iki dilimlik pencerede kabul ediliyor ve
farkın kaç dilim olduğu raporlanıyor.

Pencerenin bedeli tabloda görünüyor: her ek dilim, kabul edilen kod sayısını artırır ve
tahmin alanını genişletir. Bir dilimlik pencere (toplam üç kod) yaygın bir dengedir; daha
geniş pencere yerine sunucu saatinin doğru tutulması tercih edilir.

#### Aynı Kod İki Kez Kullanılamaz

Üçüncü bölüm tekrar kullanım savunmasını gösteriyor. Kod, geçerlilik dilimi boyunca —
yaklaşık yarım dakika — geçerlidir; bu süre içinde aynı kodun ikinci kez kabul edilmesi,
araya giren birinin kodu yeniden oynatmasına izin verir.

Çözüm, kabul edilen kodun dilimini kullanıcı bazında kaydetmektir. Kaydın ömrü pencere
kadardır; eski kayıtlar temizlenir. Bu, Kurumsal Federasyon dersindeki tekrar kullanım
listesiyle aynı kalıptır — farklı bağlamda aynı çözüm.

Deneme sınırlaması ayrı bir katmandır: altı basamaklı kod bir milyon olasılık taşır ve
sınırsız deneme hakkı verilirse pencere içinde tahmin edilebilir. Ardışık başarısız
denemeler hesap düzeyinde sayılır ve eşik aşıldığında ikinci etmen geçici olarak kilitlenir.

#### Sınıf Aynıysa Kazanç Yoktur

Dördüncü tablo etmenleri üç ölçütle karşılaştırıyor: sınıf, çevrimdışı çalışabilme ve
devredilebilirlik. Son sütun en ayırt edici olanıdır. Parola ve kurtarma kodu
**devredilebilir**: kullanıcı onları farkında olmadan başkasına söyleyebilir, bir sayfaya
yazabilir. Zaman tabanlı kod da devredilebilir görünür ama otuz saniyelik ömrü devri
zorlaştırır; donanım anahtarı ise devredilemez, çünkü imzalama işlemi cihazın içinde olur.

Tablonun son satırı bir kuralı sayıyla söylüyor: listedeki iki etmen bilgi sınıfındadır.
İkinci etmen birincinin sınıfından seçilirse — parolanın yanına bir "gizli soru" konursa —
saldırı yüzeyi değişmez, çünkü ikisi de aynı yolla ele geçirilir.

Kısa mesajla gönderilen kodun ayrı bir konumu vardır: sahiplik sınıfındadır ama çevrimdışı
çalışmaz ve teslimi üçüncü bir tarafa bağlıdır. Hiç ikinci etmen olmamasından iyidir;
bulunabildiğinde uygulama tabanlı kod ya da donanım anahtarı yeğlenir.

#### Kurtarma Kodları Hesabın Yedek Anahtarıdır

Son bölüm kurtarma kodlarını çalıştırıyor. İkinci etmen kaybedildiğinde — telefon
değiştiğinde, cihaz bozulduğunda — kullanıcının hesaba dönmesi gerekir. Kurtarma kodları bu
yolu açar ve tam da bu yüzden **hesabın en zayıf halkasıdır**.

Üç kural bunu dengeler. Kodlar tek kullanımlıktır: kullanılan kod tekrar kabul edilmez ve
kalan kod sayısı kullanıcıya gösterilir. Kodlar karma olarak saklanır; veritabanı
sızıntısında doğrudan kullanılamazlar. Ve kod kullanıldığında kullanıcıya bildirim gider —
kurtarma akışı, hesabın ele geçirilmesinin en olası yoludur ve sessiz çalışmamalıdır.

Hesap Kurtarma dersinde kurulan sıfırlama akışıyla birlikte düşünüldüğünde ölçüt şudur:
hesaba dönüş yolları, giriş yolundan daha zayıf olmamalıdır. Parolayı sıfırlamak ikinci
etmeni atlatabiliyorsa, ikinci etmen yoktur.

#### Özet

- Çok etmenli doğrulamanın kazancı kanıt sayısından değil, kanıtların farklı sınıflardan
  ve bağımsız olmasından gelir; modelde iki etmen bilgi sınıfına düşüyor.
- Zaman tabanlı kod paylaşılan sır ile zaman diliminden türetilir; saat kayması penceresi
  kabul edilen kod sayısını artırır, bu yüzden dar tutulur.
- Kabul edilen kodun dilimi kaydedilir; aynı kod ikinci kez kabul edilmez ve deneme
  sayısı ayrıca sınırlanır.
- Kısa mesajla kod sahiplik sınıfındadır ama çevrimdışı çalışmaz ve teslimi üçüncü tarafa
  bağlıdır; uygulama tabanlı kod ya da donanım anahtarı yeğlenir.
- Kurtarma kodları tek kullanımlıktır, karma olarak saklanır ve kullanıldığında bildirim
  üretir; hesaba dönüş yolu giriş yolundan zayıf olamaz.

#### Sonraki Adım

Bu konu boyunca sunucu, kullanıcının bildiği ya da sahip olduğu bir şeyi doğrulayabildiğini
varsaydı. Parolanın kendisinin nasıl saklandığı, bu doğrulamanın hangi hesapla yapıldığı ve
saklama biçiminin sızıntı durumunda ne kadar dayandığı henüz sorulmadı. Sonraki konu bu
varsayımın altına iner ve parola saklamayı ölçülebilir parametrelere bağlar.

## Kimlik Bilgisi Güvenliği

Parolanın yavaş karma algoritmasıyla saklanması ve parametre seçimi, tuz ile biberin ayrı görevleri, kullanılabilirliği bozmayan parola politikaları, sıfırlama akışının saldırı yüzeyi ve oturumun girişten sonlandırmaya kadar korunması.

### Parola Saklama

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/parola-saklama

Parolanın geri döndürülemez biçimde saklanması: yavaş karma algoritmalarının iş ve bellek katsayıları, süre bütçesine göre parametre seçimi, saklama kaydının biçimi ve karşılaştırmanın sabit zamanlı yapılması.

Önceki konu kimliğin nasıl doğrulandığını kurdu: temel şemadan oturuma, belirteçten çok
etmenli doğrulamaya kadar akışların tamamı, sunucunun "bu istek gerçekten o kişiden mi
geliyor" sorusuna verdiği yanıtlardı. Bu akışların hepsinin altında tek bir varsayım
duruyordu — sunucu, kullanıcının bildiği bir şeyi doğrulayabiliyor. Bu konu o varsayımın
kendisine bakıyor: sunucu parolayı doğrulayabilmek için ne saklıyor?

Kütüphane ödünç servisinde üyeler ve personel aynı sisteme parolayla giriyor. Sunucunun
elindeki kayıt, bir gün başkasının eline geçebilir — yedek dosyası, hatalı bir dışa
aktarım, ele geçirilmiş bir rapor hesabı. Parola saklamanın ölçütü bu andır: kayıt
sızdığında ne kadar bilgi sızmış olur? Bu dersin tamamı, o anın maliyetini düşürmekle
ilgilidir.

#### Doğrulanabilir Ama Geri Döndürülemez

Sunucunun parolayı doğrulayabilmesi için parolayı bilmesi gerekmez. Gereken, kullanıcının
gönderdiği dizgiden aynı sonucu üretebilmektir. Bu yüzden parola düz metin olarak değil,
**tek yönlü karma (one-way hash)** olarak saklanır: girdiden çıktıyı hesaplamak kolay,
çıktıdan girdiye dönmek hesaplama açısından yapılamaz olsun.

Karma fonksiyonu kavramı Veri Yapıları kursunda tanıtılmıştı; oradaki amaç kova numarası
üretmekti ve ölçüt hızdı. Parola saklamada ölçüt terse döner. Karma ne kadar hızlı
hesaplanıyorsa, sızmış bir kayıt üzerinde aday parola denemek de o kadar hızlıdır.
Doğrulamanın hızlı olması saldırganın işini kolaylaştırır; yavaş olması ise yalnızca giriş
isteğini birkaç on milisaniye geciktirir. Bu asimetri, parola saklamanın tek tasarım
kaldıracıdır.

Bu nedenle parola için genel amaçlı karma fonksiyonları değil, **anahtar türetme
fonksiyonu (key derivation function)** kullanılır. Bunlar iki ayarlanabilir katsayı taşır:
**iş katsayısı (work factor)**, hesaplamanın kaç tur süreceğini; **bellek katsayısı**,
hesaplamanın ne kadar bellek tutacağını belirler. Belleği zorunlu kılan tasarıma **bellek
sertliği (memory hardness)** denir ve amacı, hesabı çok sayıda küçük işlem birimine
dağıtmayı pahalı hâle getirmektir. `scrypt` ve `Argon2` bu sınıfın iki üyesidir; bu ders
`node` çalışma zamanında yerleşik olan `scrypt` ile ilerliyor.

#### Maliyetin Ölçülmesi

İki yaklaşımın farkı tartışılmaz, ölçülür. Aşağıdaki betik aynı makinede tek geçişli bir
karma ile farklı iş katsayılarındaki `scrypt` çağrısının doğrulama süresini karşılaştırır.
Son sütun, tek bir çekirdeğin saniyede kaç doğrulama yapabildiğidir.

```bash
cat > olcum.mjs <<'EOF'
import { createHash, scryptSync, randomBytes } from "node:crypto";

const parola = "sahil-subesi-2026";
const tuz = randomBytes(16);

function sure(kere, is) {
  is();                                        // isinma
  const basla = process.hrtime.bigint();
  for (let i = 0; i < kere; i++) is();
  return Number(process.hrtime.bigint() - basla) / kere / 1e6;   // milisaniye
}

const satirlar = [
  ["sha-256 (tek gecis)", "-", sure(500, () =>
    createHash("sha256").update(tuz).update(parola).digest())],
];

for (const N of [2 ** 12, 2 ** 14, 2 ** 16, 2 ** 17]) {
  const r = 8, p = 1;
  satirlar.push([
    `scrypt N=2^${Math.log2(N)} r=${r} p=${p}`,
    `${((128 * r * N) / 1024 / 1024).toFixed(0)} MiB`,
    sure(3, () => scryptSync(parola, tuz, 32, { N, r, p, maxmem: 512 * 1024 * 1024 })),
  ]);
}

console.log("algoritma              | bellek  | dogrulama  | cekirdek basina giris/s");
console.log("-----------------------|---------|------------|------------------------");
for (const [ad, bellek, ms] of satirlar) {
  console.log(
    ad.padEnd(22) + " | " + bellek.padStart(7) + " | " +
    (ms < 1 ? ms.toFixed(4) : ms.toFixed(1)).padStart(7) + " ms | " +
    (1000 / ms).toFixed(0).padStart(22)
  );
}
EOF
node olcum.mjs
```

```text
algoritma              | bellek  | dogrulama  | cekirdek basina giris/s
-----------------------|---------|------------|------------------------
sha-256 (tek gecis)    |       - |  0.0013 ms |                 781504
scrypt N=2^12 r=8 p=1  |   4 MiB |     4.3 ms |                    231
scrypt N=2^14 r=8 p=1  |  16 MiB |    17.6 ms |                     57
scrypt N=2^16 r=8 p=1  |  64 MiB |    79.4 ms |                     13
scrypt N=2^17 r=8 p=1  | 128 MiB |   159.5 ms |                      6
```

Mutlak süreler makineye ve çalışma zamanı sürümüne göre değişir; buradaki değerler
`node` sürüm 24 ile tek bir masaüstü çekirdeğinde alınmıştır. Değişmeyen şey büyüklük
sırasıdır. Tek geçişli karma ile `N=2^14` arasında yaklaşık on dört bin katlık bir fark
var. Bu fark, sızmış bir kayıt üzerinde aday deneyen tarafın maliyetiyle doğrudan
orantılıdır: hızlı karmayla saniyede yüz binlerce aday denenebilirken, yavaş karmayla
aynı donanımda onlarca aday denenebilir.

Bellek sütunu ikinci savunmayı gösterir. `N=2^16` için her doğrulama 64 MiB tutar. Bu
miktar tek bir sunucu için önemsizdir; binlerce koşutlu hesap birimi üzerinde aynı işi
yapmak isteyen taraf için belirleyicidir. İş katsayısını artırmak hesabı yavaşlatır,
bellek katsayısını artırmak hesabı ucuz donanıma dağıtılamaz kılar.

#### İş Katsayısının Seçimi

Parametreler "ne kadar büyükse o kadar iyi" kuralıyla seçilmez. Doğrulama sunucunun
kendi işidir; çok büyük bir katsayı, giriş isteklerinin sunucuyu kilitlemesine yol açar.
Seçimin iki kısıtı vardır: giriş başına ayrılan süre bütçesi ve tepe yükte tutulacak
bellek.

Aşağıdaki betik bu iki kısıtı birlikte uygular. Bütçe giriş başına 100 milisaniye, tepe
yük saniyede yirmi giriş kabul edilmiştir; bellek sütunu, o katsayıda aynı anda
yürüyecek doğrulamaların toplam tüketimidir.

```bash
cat > kalibre.mjs <<'EOF'
import { scryptSync, randomBytes } from "node:crypto";

const BUTCE_MS = 100;                     // giris basina ayrilan sure
const ESZAMANLI_GIRIS = 20;               // saniyede beklenen tepe giris sayisi
const tuz = randomBytes(16), r = 8, p = 1;

function olc(N) {
  const ayar = { N, r, p, maxmem: 512 * 1024 * 1024 };
  scryptSync("olcum", tuz, 32, ayar);
  const basla = process.hrtime.bigint();
  scryptSync("olcum", tuz, 32, ayar);
  return Number(process.hrtime.bigint() - basla) / 1e6;
}

console.log(`butce: ${BUTCE_MS} ms/giris, tepe yuk: ${ESZAMANLI_GIRIS} giris/s`);
console.log("N     | dogrulama | tepe yukte bellek | butce");
console.log("------|-----------|-------------------|-------");
let secilen = null;
for (const us of [12, 13, 14, 15, 16, 17]) {
  const N = 2 ** us, ms = olc(N);
  const bellek = (128 * r * N * Math.ceil(ms / 1000 * ESZAMANLI_GIRIS)) / 1024 / 1024;
  const uyar = ms <= BUTCE_MS ? "uygun" : "asildi";
  if (ms <= BUTCE_MS) secilen = us;
  console.log(`2^${us}  | ${ms.toFixed(1).padStart(6)} ms | ${bellek.toFixed(0).padStart(13)} MiB | ${uyar}`);
}
console.log("secilen: N=2^" + secilen);
EOF
node kalibre.mjs
```

```text
butce: 100 ms/giris, tepe yuk: 20 giris/s
N     | dogrulama | tepe yukte bellek | butce
------|-----------|-------------------|-------
2^12  |    5.3 ms |             4 MiB | uygun
2^13  |    8.7 ms |             8 MiB | uygun
2^14  |   18.2 ms |            16 MiB | uygun
2^15  |   37.7 ms |            32 MiB | uygun
2^16  |   77.8 ms |           128 MiB | uygun
2^17  |  162.7 ms |           512 MiB | asildi
secilen: N=2^16
```

Betiğin ürettiği sayılar makineye özgüdür; ürettiği **karar yordamı** değildir. Kural
şudur: bütçeyi aşmayan en büyük katsayı seçilir ve seçim, kodun içine gömülü bir sabit
değil, dağıtım yapılandırmasından okunan bir değer olur. Sunucu donanımı değiştiğinde
yeniden ölçülür.

Bütçenin kendisi de bir karardır. Giriş, kullanıcının bekleyeceği bir işlemdir ve yüz
milisaniye bu bekleyişte fark edilmez. Aynı hesap her istekte yapılmaz — yalnız girişte
yapılır, sonrasında oturum ya da belirteç devreye girer. Doğrulamanın her API çağrısında
tekrarlandığı bir kurulum, bu bütçeyi kullanılamaz hâle getirir; bu, parola karmasının
API anahtarı doğrulaması yerine kullanılmaması gerektiğinin nedenidir.

#### Saklama Kaydının Biçimi

Kayıtta yalnız karma durmaz. Karmayı üreten algoritma ve parametreleri de saklanır;
aksi hâlde parametreler yükseltildiğinde eski kayıtlar doğrulanamaz hâle gelir. Yerleşik
biçim, alanları tek bir dizgide ayraçla birleştirmektir.

```bash
cat > parola.mjs <<'EOF'
import { scryptSync, randomBytes, timingSafeEqual } from "node:crypto";

const AYAR = { N: 2 ** 14, r: 8, p: 1, uzunluk: 32 };

function kaydet(parola) {
  const tuz = randomBytes(16);
  const ozet = scryptSync(parola.normalize("NFKC"), tuz, AYAR.uzunluk, AYAR);
  return ["scrypt", AYAR.N, AYAR.r, AYAR.p,
          tuz.toString("base64"), ozet.toString("base64")].join("$");
}

function dogrula(parola, kayit) {
  const [alg, N, r, p, tuz64, ozet64] = kayit.split("$");
  if (alg !== "scrypt") return false;
  const beklenen = Buffer.from(ozet64, "base64");
  const uretilen = scryptSync(parola.normalize("NFKC"), Buffer.from(tuz64, "base64"),
    beklenen.length, { N: +N, r: +r, p: +p });
  return timingSafeEqual(uretilen, beklenen);
}

const kayit = kaydet("sahil-subesi-2026");
console.log("saklanan kayit :", kayit);
console.log("uzunluk        :", kayit.length, "karakter");
console.log("dogru parola   :", dogrula("sahil-subesi-2026", kayit));
console.log("yanlis parola  :", dogrula("sahil-subesi-2025", kayit));
console.log("ikinci kayit   :", kaydet("sahil-subesi-2026").slice(0, 24) + "...");
EOF
node parola.mjs
```

```text
saklanan kayit : scrypt$16384$8$1$/kFvW5D9IBMpOV7Sn9t0YQ==$JwrU7KOCzQiHUKCE8+Uazs9zrp1RhJgQVcYqhINzCsQ=
uzunluk        : 86 karakter
dogru parola   : true
yanlis parola  : false
ikinci kayit   : scrypt$16384$8$1$QVqPrxj...
```

Rastgele üretilen değerler her çalıştırmada değişir; kaydın uzunluğu ve alan düzeni
değişmez. Kayıt üç bilgi taşır: hangi algoritma, hangi parametreler, hangi rastgele değer.
Bu düzenin doğrudan sonucu, veritabanı sütununun sabit uzunlukta değil, biçim
değişikliklerine yer bırakan genişlikte tanımlanmasıdır.

Son satır bu dersin sonraki konusuna işaret eder: aynı parola ikinci kez kaydedildiğinde
kayıt farklıdır. Bunu sağlayan `randomBytes` çağrısıdır ve nedeni sonraki dersin konusu.

`normalize("NFKC")` çağrısı ayrı bir gerekçeye dayanır. Aynı görünen bir metin farklı bayt
dizileriyle kodlanabilir; kayıt sırasında bir biçim, giriş sırasında öteki biçim gelirse
doğrulama başarısız olur. Kullanıcı için bu, "doğru parolayı yazdım ama kabul etmiyor"
durumudur. Normalleştirme kayıt ve doğrulama yollarının **ikisinde birden** aynı biçimde
uygulanır; yalnız birinde uygulanması sorunu çözmez, taşır.

#### Karşılaştırmanın Süresi

Doğrulamanın son adımı iki bayt dizisini karşılaştırmaktır ve bu adımın nasıl yazıldığı
önemlidir. Erken dönen bir karşılaştırma, ilk farklı bayttan sonra durur — dolayısıyla
çalışma süresi, adayın doğruyla kaç bayt boyunca örtüştüğünü ele verir.

```bash
cat > karsilastirma.mjs <<'EOF'
import { randomBytes, timingSafeEqual } from "node:crypto";

const dogru = randomBytes(32);

function erkenDonen(a, b) {                     // ilk farkta durur
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
  return true;
}

function olc(karsilastir, aday) {
  for (let i = 0; i < 2000; i++) karsilastir(dogru, aday);      // isinma
  const basla = process.hrtime.bigint();
  for (let i = 0; i < 200000; i++) karsilastir(dogru, aday);
  return Number(process.hrtime.bigint() - basla) / 200000;       // nanosaniye
}

const ilkte = Buffer.from(dogru); ilkte[0] ^= 1;
const sonda = Buffer.from(dogru); sonda[31] ^= 1;

console.log("karsilastirma   | ilk baytta ayrilan | son baytta ayrilan | oran");
console.log("----------------|--------------------|--------------------|------");
for (const [ad, fn] of [["erken donen", erkenDonen], ["timingSafeEqual", timingSafeEqual]]) {
  const a = olc(fn, ilkte), b = olc(fn, sonda);
  console.log(
    ad.padEnd(15) + " | " + (a.toFixed(1) + " ns").padStart(18) + " | " +
    (b.toFixed(1) + " ns").padStart(18) + " | " + (b / a).toFixed(2).padStart(5)
  );
}
EOF
node karsilastirma.mjs
```

```text
karsilastirma   | ilk baytta ayrilan | son baytta ayrilan | oran
----------------|--------------------|--------------------|------
erken donen     |             3.4 ns |            22.4 ns |  6.60
timingSafeEqual |            32.9 ns |            30.6 ns |  0.93
```

Nanosaniye değerleri makineye göre değişir; oran sütunu değişmez. Erken dönen
karşılaştırmada son baytta ayrılan aday, ilk baytta ayrılandan altı kat uzun sürüyor —
yani süre, örtüşmenin uzunluğunu ölçen bir sinyaldir. `timingSafeEqual` her durumda tüm
baytları işlediği için oran bire yakın kalıyor.

Parolanın kendisinde bu sinyalin uzaktan kullanılabilirliği düşüktür, çünkü karşılaştırma
öncesinde onlarca milisaniyelik bir karma hesabı vardır ve ağ gecikmesi sinyali gürültüye
gömer. Ama aynı karşılaştırma kalıbı, karma hesabı olmayan yerlerde de kullanılır:
oturum kimliği, sıfırlama belirteci, API anahtarı, imza doğrulaması. Bu yüzden kural
gevşetilmez — gizli bir değerle yapılan her karşılaştırma sabit zamanlı yazılır.
İstemci Güvenliği tarafında aynı kural istek belirteci karşılaştırması için konmuştu;
sunucu tarafı da aynı kuralı uygular.

#### Parametrelerin Yükseltilmesi

Bugün seçilen katsayı, donanım ucuzladıkça yetersiz kalır. Yükseltme, kayıtta saklanan
parametrelerin varlığı sayesinde kesintisiz yapılabilir: kullanıcı giriş yaptığında
gönderdiği parola elde olur, doğrulama başarılıysa kayıt yeni parametrelerle yeniden
üretilip yazılır. Bu, girişteki bir ek yazma işlemidir ve kullanıcıya görünmez.

Yükseltmenin sınırı şudur: hiç giriş yapmayan hesapların kayıtları eski parametrelerde
kalır. Bu hesaplar için ikinci yol, eski karmanın üzerine yeni algoritmayı uygulayıp
kayda iki katmanlı olduğunu yazmaktır. Kayıt biçiminin algoritma alanı taşıması, bu geçişi
kayıt kaybı olmadan mümkün kılan şeydir.

Kabul edilmeyen üçüncü yol, parolaları düz metin olarak yeniden istemektir. Sistemin
parolayı yeniden isteyebilmesi, onu bir yerde tutabildiği anlamına gelir; bu dersin
tamamı bunun tersini kurar.

#### Özet

- Parola geri döndürülemez biçimde saklanır; sunucunun doğrulama için parolayı bilmesi
  gerekmez, aynı sonucu üretebilmesi yeterlidir.
- Genel amaçlı karma fonksiyonları parola için uygun değildir; iş ve bellek katsayısı
  ayarlanabilen anahtar türetme fonksiyonları kullanılır.
- İş katsayısı, giriş başına süre bütçesini aşmayan en büyük değer olarak ölçülerek
  seçilir; tepe yükteki bellek tüketimi ikinci kısıttır.
- Kayıt, karmanın yanında algoritmayı ve parametreleri de taşır; yükseltme başarılı
  girişte kaydın yeniden üretilmesiyle yapılır.
- Gizli bir değerle yapılan karşılaştırma sabit zamanlı yazılır; erken dönen
  karşılaştırmanın süresi örtüşme uzunluğunu ele verir.

#### Sonraki Adım

Bu derste her kayıt için `randomBytes` ile bir rastgele değer üretildi ve aynı parolanın
iki kaydı farklı çıktı. O değerin adı ve görevi henüz açıklanmadı. Sonraki ders tuzun ne
işe yaradığını, tuzsuz bir kurulumda aynı parolayı kullanan iki üyenin kaydının neden
birbirinin aynısı olduğunu ve bunun tek bir sızıntıdan kaç kullanıcı hakkında bilgi
çıkardığını gösteriyor. Aynı derste tuzdan ayrı ikinci bir değer olan biberin nerede
durduğu — veritabanının içinde değil, uygulamanın yapılandırmasında — ve bu ayrımın
sızıntı anında ne değiştirdiği ele alınıyor.

### Tuz ve Biber

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/tuz-ve-biber

Kayıt başına rastgele tuzun ön hesaplanmış tablolara karşı ne sağladığı, tuzsuz kurulumun sızdırdığı bilgi, tuzun ve biberin ayrı görevleri ve biberin veritabanı dışında tutulmasının sızıntı anındaki sonucu.

Önceki derste her kayıt için `randomBytes` ile on altı baytlık bir değer üretildi ve aynı
parolanın iki kaydı farklı çıktı. O değerin adı **tuz (salt)** ve görevi karmayı gizlemek
değil, kayıtlar arasındaki eşitliği bozmaktır. Bu ders tuzun tam olarak neyi engellediğini,
tuzsuz bir kurulumun neyi ele verdiğini ve tuzdan ayrı ikinci bir değer olan biberin nerede
durduğunu ele alıyor.

Soru şuradan doğar: yavaş karma seçildi, parametreler ölçülerek ayarlandı, kayıt biçimi
kuruldu. Kayıtlar sızarsa her birinin ayrı ayrı ele alınması gerekir mi, yoksa tek bir iş
tüm kayıtlara birden mi yarar? Tuz, bu iki durumu birbirinden ayıran şeydir.

#### Tuzun Görevi

Karma fonksiyonu belirlenimcidir: aynı girdi her zaman aynı çıktıyı verir. Bu özellik
doğrulamayı mümkün kılan şeydir, ama tek başına kullanıldığında istenmeyen bir sonucu
vardır — aynı parolayı seçen iki üyenin kaydı birebir aynı olur.

Tuz, karma hesabına giren ikinci bir girdidir. Her kayıt için ayrı üretilir, gizli
değildir ve karmanın yanında saklanır. Sonucu şudur: aynı parola farklı tuzlarla farklı
karmalar üretir. Doğrulama sırasında tuz kayıttan okunur, kullanıcının gönderdiği parolayla
birleştirilir, sonuç karşılaştırılır. Doğrulama akışı değişmez; değişen, kayıtlar arasındaki
ilişkidir.

#### Tuzsuz Kurulumun Gözlenebilir Sonucu

Aşağıdaki betik kütüphane üye tablosunu iki sütunla kuruyor: aynı parolaların tuzsuz ve
tuzlu karmaları. Veri yereldir ve örnek amaçlıdır; sekiz üyeden üçü aynı parolayı, ikişer
üye de iki ayrı parolayı paylaşıyor.

```bash
cat > tuz.mjs <<'EOF'
import { createHash, randomBytes, scryptSync } from "node:crypto";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync(":memory:");
db.exec(`CREATE TABLE uye(kod TEXT PRIMARY KEY, sube TEXT, tuzsuz TEXT, tuzlu TEXT)`);

// Yerel ornek veri; kurulum farkini gostermek icin.
const uyeler = [
  ["U-1001", "S-01", "deniz-kabugu-7"],
  ["U-1002", "S-01", "kutuphane"],
  ["U-1003", "S-02", "deniz-kabugu-7"],
  ["U-1004", "S-02", "kutuphane"],
  ["U-1005", "S-01", "yaz-2026-okuma"],
  ["U-1006", "S-03", "kutuphane"],
  ["U-1007", "S-03", "cinar-agaci"],
  ["U-1008", "S-02", "yaz-2026-okuma"],
];

const ayar = { N: 2 ** 12, r: 8, p: 1 };
const ekle = db.prepare("INSERT INTO uye VALUES (?,?,?,?)");
for (const [kod, sube, parola] of uyeler) {
  const tuzsuz = createHash("sha256").update(parola).digest("base64");
  const tuz = randomBytes(16);
  const tuzlu = tuz.toString("base64") + "$" +
    scryptSync(parola, tuz, 32, ayar).toString("base64");
  ekle.run(kod, sube, tuzsuz, tuzlu);
}

const yaz = (baslik, satirlar) => {
  console.log(baslik);
  for (const s of satirlar) console.log("  " + s);
};

yaz("tuzsuz kayitlar (ilk 12 karakter):",
  db.prepare("SELECT kod, substr(tuzsuz,1,12) k FROM uye ORDER BY kod").all()
    .map((s) => `${s.kod}  ${s.k}`));

yaz("\nayni karmayi paylasan uyeler:",
  db.prepare(`SELECT substr(tuzsuz,1,12) k, COUNT(*) adet, group_concat(kod,' ') kimler
              FROM uye GROUP BY tuzsuz HAVING COUNT(*) > 1`).all()
    .map((s) => `${s.k}  ${s.adet} uye: ${s.kimler}`));

yaz("\ntuzlu kayitlarda ayni gruplama:",
  db.prepare(`SELECT COUNT(*) grup FROM
              (SELECT tuzlu FROM uye GROUP BY tuzlu HAVING COUNT(*) > 1)`).all()
    .map((s) => `eslesen grup sayisi: ${s.grup}`));
EOF
node tuz.mjs
```

```text
tuzsuz kayitlar (ilk 12 karakter):
  U-1001  LCg5/0sSaTRk
  U-1002  w+UPBHK8+dLk
  U-1003  LCg5/0sSaTRk
  U-1004  w+UPBHK8+dLk
  U-1005  QHYLNUanoH3r
  U-1006  w+UPBHK8+dLk
  U-1007  djFHjvQkKdlh
  U-1008  QHYLNUanoH3r

ayni karmayi paylasan uyeler:
  LCg5/0sSaTRk  2 uye: U-1001 U-1003
  QHYLNUanoH3r  2 uye: U-1005 U-1008
  w+UPBHK8+dLk  3 uye: U-1002 U-1004 U-1006

tuzlu kayitlarda ayni gruplama:
  eslesen grup sayisi: 0
```

Tek bir `GROUP BY` sorgusu, parolaları hiç çözmeden üç bilgi üretti.

Birincisi, hangi üyelerin aynı parolayı kullandığı. U-1002, U-1004 ve U-1006 aynı dizgiyi
seçmiş. Bu üç hesap artık tek bir hesap gibidir: biri hakkında öğrenilen her şey diğer
ikisi için de geçerlidir.

İkincisi, en kalabalık grubun hangisi olduğu. Üç üyeli grup, bu tabloda en yaygın seçimi
işaret eder. Yaygın seçim, aday listelerinin başında duran seçimdir.

Üçüncüsü, bir yönetici ya da personel hesabının parolasının bir üye hesabıyla aynı olup
olmadığı. Bu, hesaplar arası bir bağdır ve tabloya bakan herkese açıktır.

Tuzlu sütunda aynı sorgu sıfır grup döndürüyor. Parolalar değişmedi; değişen, kayıtların
birbirine bakarak bilgi vermesi. Tuz bir parolayı güçlendirmez — parolalar arasındaki
görünür ilişkiyi yok eder.

#### Ön Hesaplanmış Tablonun Maliyeti

Tuzun ikinci ve daha bilinen etkisi **ön hesaplanmış tablolara (precomputed table)**
karşıdır. Aday parolaların karmaları bir kez hesaplanıp saklandığında, sızmış bir tuzsuz
kayıt için yapılacak iş bir arama işlemine iner; hesap zaten yapılmıştır. Bu tabloların
yer kazanmak için sıkıştırılmış biçimine gökkuşağı tablosu denir; kurulum tarafındaki
karşılığı ikisinde de aynıdır.

Tuz bu ekonomiyi bozar. Tuz kayıt başına farklı olduğu için, hazır tablo hiçbir kayda
uymaz; iş her kayıt için baştan yapılır. Aşağıdaki betik, önceki derste ölçülen sürelerle
bu farkı sayıya çeviriyor.

```bash
cat > maliyet.mjs <<'EOF'
const ADAY = 1_000_000;          // on hesaplanmis listedeki aday sayisi
const KULLANICI = 100_000;       // sizan kayit sayisi
const HIZLI_MS = 0.0013;         // olculen: sha-256 tek gecis
const YAVAS_MS = 18.2;           // olculen: scrypt N=2^14

function bicimle(ms) {
  const s = ms / 1000;
  if (s < 3600) return `${s.toFixed(1)} saniye`;
  const saat = s / 3600;
  if (saat < 24 * 365) return `${saat.toFixed(1)} saat`;
  return `${(saat / 24 / 365).toFixed(1)} yil`;
}

const kurulumlar = [
  ["tuzsuz + hizli karma", ADAY * HIZLI_MS, "liste bir kez uretilir, her kayda uyar"],
  ["tuzsuz + yavas karma", ADAY * YAVAS_MS, "liste bir kez uretilir, her kayda uyar"],
  ["tuzlu  + hizli karma", ADAY * KULLANICI * HIZLI_MS, "her kayit icin bastan is"],
  ["tuzlu  + yavas karma", ADAY * KULLANICI * YAVAS_MS, "her kayit icin bastan is"],
];

console.log(`aday: ${ADAY.toLocaleString("tr-TR")}   sizan kayit: ${KULLANICI.toLocaleString("tr-TR")}`);
console.log("kurulum              | tek cekirdekte is | not");
console.log("---------------------|-------------------|---------------------------------------");
for (const [ad, ms, not] of kurulumlar) {
  console.log(ad.padEnd(20) + " | " + bicimle(ms).padStart(17) + " | " + not);
}
EOF
node maliyet.mjs
```

```text
aday: 1.000.000   sizan kayit: 100.000
kurulum              | tek cekirdekte is | not
---------------------|-------------------|---------------------------------------
tuzsuz + hizli karma |        1.3 saniye | liste bir kez uretilir, her kayda uyar
tuzsuz + yavas karma |          5.1 saat | liste bir kez uretilir, her kayda uyar
tuzlu  + hizli karma |         36.1 saat | her kayit icin bastan is
tuzlu  + yavas karma |          57.7 yil | her kayit icin bastan is
```

Tablo iki savunmanın birbirini nasıl çarptığını gösteriyor. Yalnız yavaş karma, tuzsuz
kurulumda beş saatlik bir işle karşılanıyor — çünkü iş bir kez yapılıp yüz bin kaydın
tamamına uygulanıyor. Yalnız tuz, hızlı karmayla otuz altı saate çıkıyor. İkisi birlikte
kullanıldığında sayı elli yedi yıla gidiyor.

Buradaki mutlak sayılar bir tahmin değil, ölçülmüş sürelerin çarpımıdır ve donanıma göre
değişir. Değişmeyen, çarpanın yapısıdır: tuz, işi kayıt sayısıyla çarpar; yavaş karma, işi
birim maliyetle çarpar. İkisi ayrı sorunları çözer ve biri diğerinin yerine geçmez.

#### Tuzun Kuralları

Tuzun doğru kurulumu dört kuralla tanımlanır.

**Kayıt başına ayrı üretilir.** Uygulama genelinde tek bir tuz kullanmak, tuzsuz kurulumun
eşitlik sorununu olduğu gibi bırakır — aynı parolayı kullanan iki kayıt yine aynı karmayı
verir.

**Rastgele üretilir.** Kullanıcı kodundan, e-posta adresinden ya da kayıt zamanından
türetilen tuz, öngörülebilir olduğu için ön hesaplamayı yeniden mümkün kılar. Kaynak,
kriptografik rastgele sayı üretecidir.

**Yeterli uzunlukta olur.** On altı bayt yerleşik seçimdir. Ölçüt, tuzların çakışma
olasılığının önemsiz kalmasıdır; kısa tuz, farklı kayıtların aynı tuza düşmesine ve o
kayıtlar arasında eşitliğin geri gelmesine yol açar.

**Gizli değildir.** Tuz karmanın yanında, aynı satırda saklanır. Gizli tutulması gereken
bir değer değildir; görevi gizlilik değil çeşitliliktir. Tuzu gizli saymak, biberi tuz
yerine kullanmaya yol açan yaygın karışıklıktır.

Son kural, parola değiştirme akışını da belirler: parola değiştiğinde tuz da yenilenir.
Eski tuzun korunması, yeni karmanın eskisiyle karşılaştırılabilir kalmasına yol açar.

#### Biber: Veritabanının Dışındaki Değer

**Biber (pepper)** tuzdan üç noktada ayrılır: tüm kayıtlar için ortaktır, gizlidir ve
veritabanında durmaz. Uygulamanın yapılandırmasında — ortam değişkeninde ya da gizli değer
deposunda — tutulur. Sunucu Tarafı Temelleri kursunda gizli değerlerin kod ve veri
yollarından ayrılması kurulmuştu; biber o ayrımın parola tarafındaki uygulamasıdır.

Uygulanışı **anahtarlı karma (HMAC)** iledir: yavaş karmanın çıktısı, biber anahtarıyla bir
kez daha karmalanır. Kayda yazılan değer bu son çıktıdır.

```bash
cat > biber.mjs <<'EOF'
import { scryptSync, randomBytes, createHmac, timingSafeEqual } from "node:crypto";

const AYAR = { N: 2 ** 12, r: 8, p: 1 };
const BIBERLER = { v1: process.env.BIBER_V1 ?? "" };     // ortam degiskeninden okunur

function kaydet(parola, surum = "v1") {
  const tuz = randomBytes(16);
  const yavas = scryptSync(parola, tuz, 32, AYAR);
  const ozet = createHmac("sha256", BIBERLER[surum]).update(yavas).digest();
  return [surum, tuz.toString("base64"), ozet.toString("base64")].join("$");
}

function dogrula(parola, kayit) {
  const [surum, tuz64, ozet64] = kayit.split("$");
  const biber = BIBERLER[surum];
  if (biber === undefined) return "biber bulunamadi";
  const yavas = scryptSync(parola, Buffer.from(tuz64, "base64"), 32, AYAR);
  const uretilen = createHmac("sha256", biber).update(yavas).digest();
  return timingSafeEqual(uretilen, Buffer.from(ozet64, "base64"));
}

const kayit = kaydet("deniz-kabugu-7");
console.log("veritabanina yazilan  :", kayit);
console.log("biber kaydin icinde mi:", kayit.includes(BIBERLER.v1));
console.log("dogru biber, dogru parola  :", dogrula("deniz-kabugu-7", kayit));
console.log("dogru biber, yanlis parola :", dogrula("deniz-kabugu-8", kayit));

BIBERLER.v1 = "baska-bir-deger";                        // biber bilinmiyor
console.log("yanlis biber, dogru parola :", dogrula("deniz-kabugu-7", kayit));
EOF
BIBER_V1='ornek-uygulama-sirri' node biber.mjs
```

```text
veritabanina yazilan  : v1$grq+kKcerhuc05CsczIyCA==$TdBHs+6NHkjyTM7gfYgMOpd5k0yIYh9/AAsphp+3h0k=
biber kaydin icinde mi: false
dogru biber, dogru parola  : true
dogru biber, yanlis parola : false
yanlis biber, dogru parola : false
```

Tuz alanı her çalıştırmada değişir; satırların anlamı değişmez. İkinci satır biberin kayda
hiçbir biçimde girmediğini doğruluyor. Son satır ise biberin işlevini gösteriyor: parola
doğru olduğu hâlde, biber bilinmediğinde doğrulama olumsuz dönüyor.

Bunun sızıntı anındaki karşılığı şudur. Yalnız veritabanı sızmışsa — yedek dosyası, salt
okunur rapor bağlantısı, hatalı bir dışa aktarım — eldeki kayıtlar üzerinde aday denemek
işe yaramaz, çünkü hesabın son adımı için gereken anahtar orada değildir. Biber, tuzun ve
yavaş karmanın üstüne yalnız bu senaryoyu ekler.

#### Biberin Sınırları ve Döndürülmesi

Biberin koruma alanı dar tanımlanmalıdır. Uygulama sunucusu da ele geçirilmişse biber de
ele geçmiştir; o durumda kurulum, bibersiz kurulumla aynı düzeye iner. Bu yüzden biber,
tuzun ya da yavaş karmanın yerini almaz — ikisinin üstüne konur.

İkinci sınır işletimseldir. Biber kaybolursa hiçbir kullanıcı giriş yapamaz ve kayıtlardan
geri dönüş yoktur; tek çıkış, tüm parolaların sıfırlanmasıdır. Bu yüzden biber, uygulama
yapılandırmasının en dikkatli yedeklenen parçasıdır ve kütük kayıtlarına, hata iletilerine
ya da tanılama çıktısına asla girmez.

Üçüncüsü döndürmedir. Kayıttaki `v1` alanı bunun içindir: yeni bir biber eklendiğinde eski
sürüm doğrulama için yapılandırmada kalır, başarılı her girişte kayıt yeni sürümle yeniden
üretilir. Bu, önceki dersteki parametre yükseltmesiyle aynı yordamdır — iki durumda da
geçişi mümkün kılan şey, kaydın kendi üretim koşullarını taşımasıdır.

#### Özet

- Tuz kayıt başına üretilen, gizli olmayan ve karmanın yanında saklanan bir değerdir;
  görevi kayıtlar arasındaki eşitliği bozmaktır.
- Tuzsuz kurulumda tek bir gruplama sorgusu, parolalar çözülmeden aynı parolayı kullanan
  hesapları ve en yaygın seçimi ortaya çıkarır.
- Tuz, ön hesaplanmış tablonun maliyetini kayıt sayısıyla çarpar; yavaş karma birim
  maliyeti artırır. İkisi ayrı sorunları çözer.
- Tuz kayıt başına, rastgele ve yeterli uzunlukta üretilir; parola değiştiğinde yenilenir.
- Biber tüm kayıtlar için ortak, gizli ve veritabanı dışında tutulan bir anahtardır;
  yalnız veritabanının sızdığı durumda koruma sağlar ve sürüm alanıyla döndürülür.

#### Sonraki Adım

Buraya kadarki savunmaların hepsi sunucu tarafındaydı ve kullanıcının seçtiği parolaya
dokunmuyordu. Oysa gruplama sorgusunda görülen şey — üç üyenin aynı dizgiyi seçmesi —
sunucu tarafındaki hiçbir parametreyle giderilemez. Sonraki ders parola politikalarına
geçiyor: uzunluk ve karmaşıklık kurallarının kullanıcı davranışını nasıl değiştirdiğini
bir model üzerinden hesaplıyor ve sızmış parola listesine karşı denetimin, listeyi ya da
parolayı açığa çıkarmadan nasıl yapılacağını gösteriyor.

### Parola Politikaları

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/parola-politikalari

Karmaşıklık kurallarının kullanıcı davranışını nasıl daralttığı, dönemsel değiştirme kuralının kattığı gerçek fark, sızmış parola listesine karşı denetim ve karma öneki yöntemiyle parolayı açığa çıkarmayan sorgu.

Önceki dersin gruplama sorgusu tek bir şeyi gösterdi: sekiz üyeden üçü aynı dizgiyi seçmiş
ve o dizgi `kutuphane` idi. Tuz bu üç kaydın birbirine benzemesini engeller, yavaş karma
her birinin sınanmasını pahalılaştırır — ama hiçbiri seçimin kendisini değiştirmez. Sunucu
tarafındaki savunmaların bittiği yer, parola politikasının başladığı yerdir.

Politika, kullanıcıya hangi parolanın kabul edileceğini söyleyen kuraldır. Yazılması kolay
görünen bu kural, kullanılabilirlikle güvenliğin en doğrudan çarpıştığı yerdir: kural
sıkılaştıkça kullanıcı, kuralı en az çabayla karşılayan öngörülebilir bir yola girer. Bu
ders, hangi kuralın gerçekten işe yaradığını ölçmekle ilgilidir.

#### Kuralın Öngördüğü Alan ve Kullanıcının Ürettiği Kalıp

Bir parola politikasının değeri, kuramsal olarak açtığı **arama alanıyla (search space)**
ölçülmez. Ölçüt, kural karşısında kullanıcıların gerçekte ürettiği kalıbın alanıdır.

"En az sekiz karakter, büyük harf, küçük harf, rakam ve simge zorunlu" kuralını ele alalım.
Kural, doksan dört karakterlik bir kümeden sekiz konum öngörür. Kullanıcının bu kuralı en
az çabayla karşılama yolu ise bellidir: bildiği bir kelimeyi alır, ilk harfini büyütür,
sonuna bir iki rakam ve bir simge ekler. Ortaya çıkan kalıbın alanı, kuralın öngördüğü
alandan çok daha küçüktür.

Aşağıdaki betik bu farkı dört politika için hesaplıyor. Kalıp alanları bir modeldir;
varsayımlar kodun içinde açıkça yazılıdır ve değiştirilerek yeniden hesaplanabilir.

```bash
cat > politika.mjs <<'EOF'
const bit = (n) => Math.log2(n);

const SOZLUK = 20000;      // kullanicinin sectigi taban kelime sayisi (model)
const politikalar = [
  {
    ad: "en az 8, dort tur karakter",
    kuramsal: 94 ** 8,
    // Kurali karsilayan en kisa yol: kelime + bas harf buyuk + 1-2 rakam + 1 simge
    kalip: SOZLUK * 1 * 110 * 10,
    aciklama: "kelime + Bas harf + rakam + simge",
  },
  { ad: "en az 12, kural yok", kuramsal: 26 ** 12,
    kalip: SOZLUK * SOZLUK * 100, aciklama: "iki kelime + rakam" },
  { ad: "en az 16, kural yok", kuramsal: 26 ** 16,
    kalip: SOZLUK ** 3 * 10, aciklama: "uc kelime + rakam" },
  { ad: "4 sozcuklu parola (2048 liste)", kuramsal: 2048 ** 4,
    kalip: 2048 ** 4, aciklama: "secim rastgele, kalip yok" },
];

console.log("politika                       | kuramsal | kalip    | kayip | kalibin kaynagi");
console.log("-------------------------------|----------|----------|-------|----------------------------");
for (const p of politikalar) {
  console.log(
    p.ad.padEnd(30) + " | " +
    (bit(p.kuramsal).toFixed(1) + " bit").padStart(8) + " | " +
    (bit(p.kalip).toFixed(1) + " bit").padStart(8) + " | " +
    (bit(p.kuramsal / p.kalip).toFixed(1)).padStart(5) + " | " + p.aciklama
  );
}

console.log("\n90 gunde bir degistirme kurali, kullanici sayaci ilerletiyorsa:");
for (const donem of [1, 4, 8, 20]) {
  const alan = SOZLUK * 110 * 10 * donem;
  console.log(`  ${String(donem).padStart(2)} donem sonra: ${bit(alan).toFixed(1)} bit ` +
    `(ilk donemden fark: ${(bit(alan) - bit(SOZLUK * 110 * 10)).toFixed(1)} bit)`);
}
EOF
node politika.mjs
```

```text
politika                       | kuramsal | kalip    | kayip | kalibin kaynagi
-------------------------------|----------|----------|-------|----------------------------
en az 8, dort tur karakter     | 52.4 bit | 24.4 bit |  28.0 | kelime + Bas harf + rakam + simge
en az 12, kural yok            | 56.4 bit | 35.2 bit |  21.2 | iki kelime + rakam
en az 16, kural yok            | 75.2 bit | 46.2 bit |  29.0 | uc kelime + rakam
4 sozcuklu parola (2048 liste) | 44.0 bit | 44.0 bit |   0.0 | secim rastgele, kalip yok

90 gunde bir degistirme kurali, kullanici sayaci ilerletiyorsa:
   1 donem sonra: 24.4 bit (ilk donemden fark: 0.0 bit)
   4 donem sonra: 26.4 bit (ilk donemden fark: 2.0 bit)
   8 donem sonra: 27.4 bit (ilk donemden fark: 3.0 bit)
  20 donem sonra: 28.7 bit (ilk donemden fark: 4.3 bit)
```

Birinci satır politikanın en yaygın biçimini gösteriyor: kural elli iki bit vaat ediyor,
kullanıcının kalıbı yirmi dört bit veriyor. Aradaki yirmi sekiz bitlik fark, kuralın
kullanıcıyı ittiği yönün sonucudur — kural karmaşıklığı zorunlu kıldığı için kullanıcı
karmaşıklığı en ucuz yerden, sondan ekler.

İkinci ve üçüncü satır uzunluğun ne yaptığını gösteriyor. Hiçbir karakter kuralı olmadan,
yalnız asgari uzunluğu artırmak kalıp alanını yirmi dört bitten otuz beşe, sonra kırk
altıya çıkarıyor. Uzunluk, kullanıcıyı daha fazla seçim yapmaya zorlar; karmaşıklık ise
aynı seçimi süslemeye zorlar.

Dördüncü satır sınırı gösteriyor: seçim kullanıcıya değil rastgele üretece bırakıldığında
kalıp diye bir şey kalmaz ve kuramsal alan gerçek alana eşit olur. Kırk dört bit,
birincisinden yirmi bit fazladır ve akılda tutulması ondan daha kolaydır.

#### Dönemsel Değiştirme Kuralı

Çıktının ikinci bölümü, yaygın bir işletim kuralının bilançosudur. Doksan günde bir parola
değiştirmeyi zorunlu kılan kural, kullanıcı sondaki sayacı ilerlettiğinde yirmi dönem
sonunda toplam dört buçuk bit katar. Aynı kullanıcının aynı parolayı yirmi kez yazması
karşılığında elde edilen budur.

Karşı hanede duran maliyet ölçülebilir değil ama gözlenebilirdir: her dönem başında artan
sıfırlama istekleri, yazılı not alma davranışı ve sondaki sayacın öngörülebilirliği.
Dönemsel değiştirme kuralının savunulabilir tek biçimi koşulludur — parolanın sızdığına
dair bir belirti varsa değiştirme zorunlu kılınır. Takvime bağlı zorunlu değiştirme,
ölçülen katkısı kadar zarar üretir.

#### Sızmış Parola Listesine Karşı Denetim

Kalıp tablosunun asıl sonucu şudur: kullanıcının seçimini kurallarla yönlendirmek zayıf bir
araçtır. Güçlü araç, seçimi doğrudan reddetmektir. Daha önce sızmış olduğu bilinen bir
parola, kaç kural karşılarsa karşılasın, aday listelerinin içindedir ve reddedilmelidir.

Bu denetimin uygulanmasında bir sorun vardır: liste büyüktür ve kullanıcının seçtiği
parolanın listeyi tutan tarafa gönderilmesi kabul edilemez. Çözüm, sorguyu parolanın
kendisi yerine karmasının bir **öneki (prefix)** üzerinden yapmaktır. Sunucu, önekle
eşleşen tüm kayıtları döndürür; kullanıcının parolasının o küme içinde olup olmadığına
istemci kendi belleğinde karar verir. Sunucunun öğrendiği tek şey önektir; hangi kaydın
arandığı belirsiz kalır. Bu belirsizliğe **k-anonimlik (k-anonymity)** denir ve `k`, dönen
kümedeki kayıt sayısıdır.

Önek uzunluğu bir ödünleşimdir. Kısa önek daha büyük kova döndürür — daha çok gizlilik,
daha çok veri aktarımı. Uzun önek tersini yapar. Aşağıdaki betik bu ödünleşimi yerel bir
liste üzerinde ölçüyor.

```bash
cat > onek.mjs <<'EOF'
import { createHash } from "node:crypto";

const kok = ["kutuphane","kitap","okuma","sube","deniz","cinar","yaz","kis","uye","odunc",
             "masa","raf","kart","ceza","iade","katalog","salon","kalem","defter","not"];

function* liste(n = 200000) {                     // yerel, uretilmis ornek liste
  yield* ["kutuphane", "sahil-subesi", "odunc123"];
  for (let i = 0; i < n; i++) {
    yield `${kok[i % 20]}-${kok[Math.floor(i / 20) % 20]}-${Math.floor(i / 400) % 500}`;
  }
}

const karmalar = [...liste()].map((p) =>
  createHash("sha1").update(p).digest("hex").toUpperCase());
console.log(`yerel liste: ${karmalar.length.toLocaleString("tr-TR")} kayit`);

console.log("\nonek | kova sayisi | ortalama kova | en buyuk kova | sunucunun gordugu");
console.log("-----|-------------|---------------|---------------|-------------------");
for (const uzunluk of [2, 3, 4, 5]) {
  const kova = new Map();
  for (const h of karmalar) {
    const o = h.slice(0, uzunluk);
    kova.set(o, (kova.get(o) ?? 0) + 1);
  }
  const boyutlar = [...kova.values()];
  const ort = boyutlar.reduce((a, b) => a + b, 0) / boyutlar.length;
  const enbuyuk = boyutlar.reduce((a, b) => (b > a ? b : a), 0);
  console.log(
    ` ${uzunluk}   | ${String(kova.size).padStart(11)} | ${ort.toFixed(1).padStart(13)} | ` +
    `${String(enbuyuk).padStart(13)} | ${(uzunluk * 4)} bit / 160 bit`
  );
}
EOF
node onek.mjs
```

```text
yerel liste: 200.003 kayit

onek | kova sayisi | ortalama kova | en buyuk kova | sunucunun gordugu
-----|-------------|---------------|---------------|-------------------
 2   |         256 |         781.3 |           843 | 8 bit / 160 bit
 3   |        4096 |          48.8 |            74 | 12 bit / 160 bit
 4   |       62463 |           3.2 |            13 | 16 bit / 160 bit
 5   |      181985 |           1.1 |             5 | 20 bit / 160 bit
```

Tablo önek uzunluğunun liste boyutuna bağlı seçildiğini gösteriyor. İki yüz bin kayıtlık bu
listede dört karakterlik önek ortalama üç kayıt döndürüyor; bu, k-anonimliğin anlamlı
olmadığı bir kova boyutudur. Üç karakter, ortalama kırk dokuz kayıtlık kova verir ve sunucu
yüz altmış bitlik karmanın yalnız on ikisini görür. Liste milyarlarca kayda çıktığında aynı
kova boyutuna beş karakterlik önekle ulaşılır — kural, önek uzunluğunun kova boyutunu
hedeflenen `k` değerinde tutacak biçimde seçilmesidir.

Buradaki karma fonksiyonunun seçimi ayrı bir noktadır. Bu hesap parola saklamak için değil,
kayıtları önek kovalarına dağıtmak için yapılır; hız burada bir kusur değil gerekliliktir.
Önceki iki dersin yavaş karma kuralı saklama yolunda geçerlidir, bu dizin anahtarında
değil.

#### Sorgunun Çalıştırılması

Aşağıdaki blok yerel bir aralık servisi ayağa kaldırıyor ve iki parolayı denetliyor:
önceki dersteki üyelerin paylaştığı `kutuphane` ile U-1001'in seçtiği `deniz-kabugu-7`.
İstekte giden tek şey üç karakterlik önektir.

```bash
cat > aralik-sunucu.mjs <<'EOF'
import { createServer } from "node:http";
import { createHash } from "node:crypto";

const kok = ["kutuphane","kitap","okuma","sube","deniz","cinar","yaz","kis","uye","odunc",
             "masa","raf","kart","ceza","iade","katalog","salon","kalem","defter","not"];
function* liste(n = 200000) {
  yield* ["kutuphane", "sahil-subesi", "odunc123"];
  for (let i = 0; i < n; i++) {
    yield `${kok[i % 20]}-${kok[Math.floor(i / 20) % 20]}-${Math.floor(i / 400) % 500}`;
  }
}

const ONEK = 3;                                   // bu liste boyutuna gore secildi
const kova = new Map();
for (const p of liste()) {
  const h = createHash("sha1").update(p).digest("hex").toUpperCase();
  const o = h.slice(0, ONEK);
  if (!kova.has(o)) kova.set(o, []);
  kova.get(o).push(h.slice(ONEK));
}

createServer((istek, yanit) => {
  const yol = new URL(istek.url, "http://yerel").pathname;
  if (yol === "/saglik") return yanit.writeHead(200).end("hazir\n");
  const eslesme = /^\/aralik\/([0-9A-F]{3})$/.exec(yol);
  if (!eslesme) return yanit.writeHead(404).end("");
  yanit.writeHead(200, { "content-type": "text/plain" });
  yanit.end((kova.get(eslesme[1]) ?? []).join("\n") + "\n");
}).listen(8493, "127.0.0.1");
EOF
node aralik-sunucu.mjs & sunucu=$!
until curl -sf http://127.0.0.1:8493/saglik > /dev/null; do :; done

karma() { node -e 'const {createHash}=require("node:crypto");
  process.stdout.write(createHash("sha1").update(process.argv[1]).digest("hex").toUpperCase())' "$1"; }

for parola in kutuphane deniz-kabugu-7; do
  tam=$(karma "$parola"); onek=${tam:0:3}; kuyruk=${tam:3}
  curl -s "http://127.0.0.1:8493/aralik/$onek" -o kova.txt
  echo "parola      : $parola"
  echo "  giden onek: $onek   (tam karmanin ilk 3 karakteri)"
  echo "  donen kova: $(wc -l < kova.txt | tr -d ' ') kuyruk"
  if grep -qx "$kuyruk" kova.txt; then echo "  sonuc     : listede VAR, reddedilir"
  else echo "  sonuc     : listede yok, kabul edilir"; fi
done

kill $sunucu
rm -f kova.txt aralik-sunucu.mjs
```

```text
parola      : kutuphane
  giden onek: 2E5   (tam karmanin ilk 3 karakteri)
  donen kova: 51 kuyruk
  sonuc     : listede VAR, reddedilir
parola      : deniz-kabugu-7
  giden onek: 5B8   (tam karmanin ilk 3 karakteri)
  donen kova: 54 kuyruk
  sonuc     : listede yok, kabul edilir
```

Kararı veren taraf istemcidir; sunucu yalnız bir kova döndürür ve hangi kaydın sorulduğunu
bilmez. Önek değerleri listeye ve karma fonksiyonuna bağlıdır, kova boyutları da öyle;
akış değişmez. Aynı yordam kendi altyapınızda tutulan bir listeyle de çalışır — o durumda
liste zaten sizdedir ve önek katmanı, denetimi yapan servisin parolayı hiç görmemesini
sağlar.

Denetimin nerede yapılacağı ayrı bir karardır. Kayıt ve parola değiştirme akışlarında
denetim zorunludur. Giriş akışında da yapılabilir: kullanıcı doğru parolayla girdiğinde
parola elde olur ve listeye karşı denetlenebilir; listede çıkarsa kullanıcıya değiştirme
istemi gösterilir. Bu, önceki derslerdeki parametre yükseltmesiyle aynı yerde yapılan bir
işlemdir.

#### Politikanın Yazılı Hâli

Buraya kadarki ölçümler tek bir politikaya varır.

**Asgari uzunluk konur, azami uzunluk cömert tutulur.** Sekiz karakterlik alt sınır
düşüktür; on iki daha savunulabilirdir. Üst sınır kullanıcıyı kısıtlamak için değil, uzun
girdiyle kurulan hizmet engellemesini kesmek için vardır: bellek sert bir türetme işlevinin
maliyeti girdi uzunluğuyla birlikte artar, dolayısıyla "karma nasılsa sabit uzunlukta çıktı
üretiyor" gerekçesi sınırı gereksiz kılmaz. Yaygın öneri en az 64 karakteri kabul edip
sınırı yüzlerin üstünde bir yere koymaktır. Kırk karakterde kesen bir alan ise parola
yöneticisi kullanan kullanıcıyı elle yazmaya zorlar.

**Karakter türü zorunluluğu konmaz.** Ölçülen etkisi, kullanıcıyı öngörülebilir bir kalıba
itmektir. Kuralın yerini uzunluk ve liste denetimi alır.

**Tüm karakterler kabul edilir.** Boşluk, noktalama ve Türkçe harfler dâhil. Kısıtlanan her
karakter kümesi, kullanıcının seçim alanını daraltır. Önceki derste kurulan normalleştirme
adımı bu kabulü güvenli kılar.

**Yapıştırma engellenmez.** Yapıştırmayı engelleyen alan, parola yöneticisi kullanımını
imkânsızlaştırır ve kullanıcıyı akılda tutulabilir — yani zayıf — parolaya iter.

**Geri bildirim yazarken verilir.** Kullanıcı parolayı yazarken kabul edilip edilmeyeceğini
görmelidir; gönderdikten sonra reddedilen bir form, kullanıcıyı kuralı en az çabayla
karşılamaya iter.

**Liste denetimi zorunludur.** Sızmış parola listesine karşı denetim, tüm karakter
kurallarının toplamından daha etkilidir ve yukarıdaki yöntemle parolayı açığa çıkarmadan
yapılır.

#### Özet

- Politikanın değeri kuramsal arama alanıyla değil, kural karşısında kullanıcıların
  ürettiği kalıbın alanıyla ölçülür.
- Karakter türü zorunluluğu kalıp alanını daraltır; asgari uzunluğu artırmak aynı kullanıcı
  davranışında daha geniş alan üretir.
- Takvime bağlı dönemsel değiştirme yirmi dönemde dört buçuk bit katar; savunulabilir
  biçimi, sızıntı belirtisine bağlı koşullu değiştirmedir.
- Sızmış parola listesine karşı denetim, kullanıcının seçimini kurallarla yönlendirmekten
  daha etkilidir.
- Karma önekiyle sorgulama, parolayı ve tam karmayı açığa çıkarmadan denetim yapar; önek
  uzunluğu, kova boyutunu hedeflenen `k` değerinde tutacak biçimde seçilir.

#### Sonraki Adım

Parola ne kadar iyi seçilirse seçilsin unutulur ve her sistem bir kurtarma yolu sunmak
zorundadır. Bu yol, kimlik doğrulamanın tamamını atlayan ikinci bir kapıdır: doğru
kurulmazsa, parolayı hiç bilmeyen birinin hesaba girmesine yeter. Sonraki ders sıfırlama
akışının saldırı yüzeyini ele alıyor — belirtecin tek kullanımlık, kısa ömürlü ve karma
olarak saklanması, kullanıcı numaralandırmasına karşı aynı yanıtın döndürülmesi ve
sıfırlamanın oturumlar üzerindeki etkisi.

### Hesap Kurtarma

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/hesap-kurtarma

Parola sıfırlama akışının saldırı yüzeyi: belirtecin rastgele üretilmesi, karma olarak saklanması, tek kullanımlık ve kısa ömürlü olması, kullanıcı numaralandırmasına karşı aynı yanıt ve sıfırlamanın oturumlar üzerindeki etkisi.

Önceki ders parolanın nasıl seçileceğini kurdu. Bu ders, parolanın unutulduğu durumu ele
alıyor — ve o durum, sistemin en dikkatli kurulmuş kimlik doğrulama akışını tümüyle atlayan
ikinci bir kapıdır. Kütüphane üyesi parolasını hatırlamadığında, sistem onu parolasız
tanımak zorundadır. Bu tanımanın nasıl yapıldığı, önceki üç dersteki tüm savunmaların
üzerine kurulan bir sınırdır: kurtarma akışı zayıfsa, parolanın ne kadar iyi saklandığının
önemi kalmaz.

Akışın iskeleti üç adımdır. Kullanıcı bir adres bildirir; sistem o adrese, yalnız o adrese
ulaşabilen birinin eline geçecek bir değer gönderir; o değeri geri getiren kişi yeni parola
belirler. Dersin tamamı ikinci adımdaki değerin — **sıfırlama belirtecinin (reset token)** —
özellikleriyle, birinci adımdaki yanıtın ne söylediğiyle ilgilidir.

#### Belirtecin Dört Özelliği

Sıfırlama belirteci, kısa ömürlü bir kimlik bilgisidir ve parolayla aynı özeni gerektirir.

**Rastgele üretilir.** Kullanıcı numarasından, zaman damgasından ya da sıralı bir sayaçtan
türetilen belirteç öngörülebilirdir. Kaynak, kriptografik rastgele sayı üretecidir ve
uzunluk en az otuz iki bayttır.

**Karma olarak saklanır.** Veritabanında duran değer belirtecin kendisi değil, karmasıdır.
Bunun gerekçesi parola karmasıyla aynıdır: veritabanını okuyabilen biri, açık bekleyen
sıfırlama isteklerini kullanabilecek durumda olmamalıdır. Belirteç yüksek entropili
olduğu için burada yavaş karma gerekmez; tek geçişli bir karma yeterlidir.

**Tek kullanımlıktır.** İlk kullanımda kapanır. Kapanmayan bir belirteç, e-posta kutusuna
sonradan erişen birinin hesabı yeniden ele geçirmesine yeter.

**Kısa ömürlüdür.** On beş dakikalık bir pencere yerleşik seçimdir; ölçüt, kullanıcının
e-postasını açıp bağlantıya tıklaması için gereken süredir. Uzun ömür, belirteci
posta kutusunda bekleyen kalıcı bir anahtara çevirir.

Aşağıdaki betik bu dört özelliği bir arada kuruyor ve son ikisini sorguyla kanıtlıyor.

```bash
cat > kurtarma.mjs <<'EOF'
import { randomBytes, createHash } from "node:crypto";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync(":memory:");
db.exec(`CREATE TABLE sifirlama(
  karma TEXT PRIMARY KEY, uye TEXT NOT NULL,
  uretim INTEGER NOT NULL, bitis INTEGER NOT NULL, kullanim INTEGER)`);

const karmala = (b) => createHash("sha256").update(b).digest("hex");
const simdi = () => Date.now();
const OMUR_MS = 15 * 60 * 1000;                    // 15 dakika

function uret(uye, omur = OMUR_MS) {
  const belirtec = randomBytes(32).toString("base64url");
  db.prepare("INSERT INTO sifirlama VALUES (?,?,?,?,NULL)")
    .run(karmala(belirtec), uye, simdi(), simdi() + omur);
  return belirtec;                                  // yalniz baglantida gider
}

function kullan(belirtec) {
  const kayit = db.prepare("SELECT * FROM sifirlama WHERE karma = ?").get(karmala(belirtec));
  if (!kayit) return "gecersiz";
  if (kayit.kullanim !== null) return "zaten kullanilmis";
  if (simdi() > kayit.bitis) return "suresi dolmus";
  db.prepare("UPDATE sifirlama SET kullanim = ? WHERE karma = ? AND kullanim IS NULL")
    .run(simdi(), kayit.karma);
  return `kabul: ${kayit.uye}`;
}

const gecerli = uret("U-1001");
const eskimis = uret("U-1005", -1000);              // uretildigi anda suresi dolmus

console.log("uretilen belirtec (baglantida giden):", gecerli.slice(0, 16) + "...");
console.log("veritabaninda duran   :",
  db.prepare("SELECT karma FROM sifirlama WHERE uye='U-1001'").get().karma.slice(0, 16) + "...");
console.log("belirtec kayitta mi   :",
  db.prepare("SELECT COUNT(*) s FROM sifirlama WHERE karma = ?").get(gecerli).s > 0);

console.log("\nilk kullanim          :", kullan(gecerli));
console.log("ikinci kullanim       :", kullan(gecerli));
console.log("suresi dolmus belirtec:", kullan(eskimis));
console.log("bilinmeyen belirtec   :", kullan(randomBytes(32).toString("base64url")));

console.log("\nkayit durumu:");
for (const s of db.prepare(
  `SELECT uye, substr(karma,1,10) k, (bitis - uretim)/1000 omur_sn,
          CASE WHEN kullanim IS NULL THEN 'acik' ELSE 'kapali' END durum
   FROM sifirlama ORDER BY uye`).all()) {
  console.log(`  ${s.uye}  ${s.k}  omur=${s.omur_sn} sn  durum=${s.durum}`);
}
EOF
node kurtarma.mjs
```

```text
uretilen belirtec (baglantida giden): c8l-YUVmIfuvctZL...
veritabaninda duran   : 788db50ac4677fe2...
belirtec kayitta mi   : false

ilk kullanim          : kabul: U-1001
ikinci kullanim       : zaten kullanilmis
suresi dolmus belirtec: suresi dolmus
bilinmeyen belirtec   : gecersiz

kayit durumu:
  U-1001  788db50ac4  omur=900 sn  durum=kapali
  U-1005  80c7c5ee03  omur=-1 sn  durum=acik
```

Belirteç değerleri her çalıştırmada değişir; satırların anlamı değişmez. Üçüncü satır
saklama kararının doğrulamasıdır: belirtecin kendisiyle yapılan sorgu hiçbir kayıt
bulmuyor, çünkü tabloda belirteç değil karması duruyor.

Sonraki dört satır belirtecin durum çizelgesini gösteriyor. İlk kullanım kabul edildi,
ikincisi reddedildi, süresi geçmiş olan reddedildi, hiç üretilmemiş olan reddedildi. Bu
dört sonucun her biri ayrı bir kod yoludur ve her biri ayrı sınanmalıdır; en sık atlanan
ikincisidir.

Güncelleme deyimindeki `AND kullanim IS NULL` koşulu bir ayrıntı değildir. İki eşzamanlı
istek aynı belirteci getirirse, koşulsuz bir güncelleme ikisini de kabul eder; koşullu
güncellemede yalnız biri satırı değiştirir. İleri SQL kursundaki koşullu güncelleme
kalıbının buradaki karşılığı budur.

#### Kullanıcı Numaralandırması

Akışın ilk adımı, kullanıcının bir adres bildirmesidir. Sunucunun bu isteğe verdiği yanıt,
o adresin sistemde kayıtlı olup olmadığını söylememelidir. Söylerse, akış bir üyelik sorgusu
aracına dönüşür: elindeki adres listesini tek tek deneyen biri, hangilerinin kütüphaneye üye
olduğunu öğrenir. Buna **kullanıcı numaralandırması (user enumeration)** denir.

Sızan şey parola değildir, ama önemsiz de değildir. Bir kütüphane için üyelik bilgisi
kişiseldir; başka alanlarda aynı sızıntı çok daha ağır sonuçlar doğurur.

Aşağıdaki blok iki kurulumu yan yana çalıştırıyor. Birincisi reddedilmesi gereken
kurulumdur ve yalnız karşılaştırma için buradadır.

```bash
cat > kurtarma-sunucu.mjs <<'EOF'
import { createServer } from "node:http";
import { randomBytes, createHash, scryptSync } from "node:crypto";

const UYELER = new Map([["ayse@ornek.gecersiz", "U-1001"], ["mert@ornek.gecersiz", "U-1005"]]);
const TABAN_MS = 120;                       // her iki dalda da harcanan taban sure

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

function belirtecUret(uye) {                // gercek isin maliyeti: uretim + karma
  const b = randomBytes(32).toString("base64url");
  scryptSync(b, uye, 32, { N: 2 ** 12, r: 8, p: 1 });
  return createHash("sha256").update(b).digest("hex");
}

createServer(async (istek, yanit) => {
  const yol = new URL(istek.url, "http://yerel").pathname;
  if (yol === "/saglik") return yanit.writeHead(200).end("hazir\n");

  const { eposta } = await govde(istek);
  const uye = UYELER.get(eposta);

  if (yol === "/kusurlu/sifirlama") {        // reddedilmesi gereken kurulum
    if (!uye) return yanit.writeHead(404, { "content-type": "application/json" })
      .end(JSON.stringify({ hata: "kayitli_olmayan_adres" }) + "\n");
    belirtecUret(uye);
    return yanit.writeHead(200, { "content-type": "application/json" })
      .end(JSON.stringify({ durum: "gonderildi", uye }) + "\n");
  }

  const basla = Date.now();
  if (uye) belirtecUret(uye); else belirtecUret("yok");   // ayni is her iki dalda
  const kalan = TABAN_MS - (Date.now() - basla);
  setTimeout(() => {
    yanit.writeHead(202, { "content-type": "application/json" })
      .end(JSON.stringify({ durum: "istek alindi" }) + "\n");
  }, kalan > 0 ? kalan : 0);
}).listen(8494, "127.0.0.1");
EOF
node kurtarma-sunucu.mjs & sunucu=$!
until curl -sf http://127.0.0.1:8494/saglik > /dev/null; do :; done

dene() {
  curl -s -X POST "http://127.0.0.1:8494$1" -H 'content-type: application/json' \
    -d "{\"eposta\":\"$2\"}" -w '  HTTP %{http_code}  sure=%{time_total} sn\n'
}

echo "--- reddedilmesi gereken kurulum ---"
echo "kayitli adres:";   dene /kusurlu/sifirlama ayse@ornek.gecersiz
echo "kayitsiz adres:";  dene /kusurlu/sifirlama kimse@ornek.gecersiz

echo "--- dogru kurulum ---"
echo "kayitli adres:";   dene /sifirlama ayse@ornek.gecersiz
echo "kayitsiz adres:";  dene /sifirlama kimse@ornek.gecersiz

kill $sunucu
rm -f kurtarma-sunucu.mjs
```

```text
--- reddedilmesi gereken kurulum ---
kayitli adres:
{"durum":"gonderildi","uye":"U-1001"}
  HTTP 200  sure=0.005558 sn
kayitsiz adres:
{"hata":"kayitli_olmayan_adres"}
  HTTP 404  sure=0.000523 sn
--- dogru kurulum ---
kayitli adres:
{"durum":"istek alindi"}
  HTTP 202  sure=0.121078 sn
kayitsiz adres:
{"durum":"istek alindi"}
  HTTP 202  sure=0.122348 sn
```

Süre değerleri makineye göre değişir; ayrışma örüntüsü değişmez. Reddedilmesi gereken
kurulum üç ayrı kanaldan bilgi veriyor.

Birincisi durum kodudur: 200 ile 404, iki ayrı sonucu adlandırıyor. İkincisi gövdedir;
kayıtlı adres için üye numarası bile dönüyor. Üçüncüsü süredir — kayıtlı adres için yapılan
belirteç üretimi ölçülebilir bir gecikme bırakıyor, kayıtsız adres anında dönüyor. İlk
ikisi düzeltilip üçüncüsü bırakılırsa sızıntı sürer; bu yüzden üçü birlikte ele alınır.

Doğru kurulumda üç kanal da kapalı: aynı kod, aynı gövde, aynı süre. Sunucu her iki dalda
da aynı işi yapıyor ve yanıtı sabit bir taban süreye oturtuyor. Kod 202'dir, çünkü
söylenen şey "istek alındı"dır; "gönderildi" demek, gönderilebildiği bilgisini vermektir.

Aynı kural akışın ikinci adımında da geçerlidir. Geçersiz, kullanılmış ve süresi dolmuş
belirteçler için kullanıcıya gösterilen ileti tektir: "bu bağlantı artık geçerli değil,
yeniden isteyin." Ayrı iletiler, belirtecin hangi durumda olduğunu ve dolaylı olarak
hesabın varlığını bildirir. Kütük kayıtlarında ise ayrım korunur; işletim tarafının hangi
yolun çalıştığını bilmesi gerekir.

#### Belirtecin Taşınması

Belirteç bir bağlantı içinde taşınır ve bu taşımanın kendi kuralları vardır.

Adres, ziyaret edildiği anda referans başlığıyla üçüncü taraflara sızabilir. Sıfırlama
sayfası dış kaynak yüklememeli ve referans politikası sıkı olmalıdır; İstemci Güvenliği
konusunda tanımlanan referans politikası kararı burada doğrudan uygulanır.

İkinci kural kütüklerdedir. Adres bileşenleri sunucu erişim kütüklerine yazılır; belirteç
sorgu bölümünde taşınıyorsa kütüklere de düşer. Sunucu Tarafı Temelleri kursunda kurulan
maskeleme kuralı sıfırlama adreslerini de kapsamalıdır.

Üçüncüsü, sıfırlama sayfasının belirteci kalıcı olarak saklamamasıdır. Sayfa açıldığında
belirteç bir kez okunur, yeni parola gönderilirken kullanılır ve adres çubuğundan
temizlenir.

#### Sıfırlamanın Yan Etkileri

Başarılı bir sıfırlama yalnız parolayı değiştirmez.

**Açık oturumlar sonlandırılır.** Parolanın sıfırlanması, hesabın ele geçirilmiş olabileceği
anlamına gelir; o hesaba ait tüm oturumların kapatılması, sıfırlama işleminin ayrılmaz
parçasıdır. Bunun nasıl yapıldığı sonraki dersin konusudur.

**Diğer açık sıfırlama belirteçleri kapatılır.** Aynı hesap için birden çok istek yapılmış
olabilir; kullanılmayanlar aynı işlemde geçersizleşir.

**Kullanıcıya bilgi verilir.** Parolanın değiştirildiği, kayıtlı adrese ayrıca bildirilir.
Bu bildirim, işlemi kullanıcı yapmadıysa fark edilmesini sağlayan tek düzenektir.

**Otomatik giriş yapılmaz.** Sıfırlama sonrası kullanıcı, yeni parolasıyla normal giriş
akışından geçer. Sıfırlama akışının doğrudan oturum açması, kurtarma kapısını giriş
kapısıyla eşitler.

Son olarak, istek adımı **hız sınırına (rate limiting)** tabidir. Sınır hem adres başına hem
kaynak adres başına konur; aksi hâlde akış, adres listesi denemek için ölçeklenebilir bir
araç hâline gelir. Sınır aşıldığında dönen yanıt da ayrışmamalıdır — aynı 202, aynı gövde.

#### Özet

- Sıfırlama belirteci rastgele üretilir, karma olarak saklanır, tek kullanımlıktır ve kısa
  ömürlüdür; dört özelliğin her biri ayrı bir kod yolunda sınanır.
- Belirtecin karma olarak saklanması, veritabanını okuyabilen birinin açık sıfırlama
  isteklerini kullanmasını engeller.
- Kullanıcı numaralandırması durum kodu, gövde ve süre olmak üzere üç kanaldan sızar;
  üçü birlikte kapatılmadıkça sızıntı sürer.
- Belirteç bağlantıda taşınır; referans başlığı, erişim kütükleri ve adres çubuğu bu
  taşımanın sızıntı noktalarıdır.
- Başarılı sıfırlama açık oturumları ve diğer belirteçleri kapatır, kullanıcıya bildirim
  gönderir ve otomatik giriş yapmaz.

#### Sonraki Adım

Bu dersin son kuralı bir sonraki dersin sorusunu doğuruyor: "hesaba ait tüm oturumları
kapat" cümlesinin karşılığı nedir? Oturum kimliği tarayıcıda duruyorsa sunucu onu nasıl
geçersizleştirir, kullanıcı hangi aygıtlardan girmiş olduğunu nereden görür? Sonraki ders
oturum güvenliğine geçiyor: girişte oturum kimliğinin yenilenmesi, eşzamanlı oturumların
listelenmesi ve tek tek sonlandırılması, boşta kalma ile mutlak ömür sınırlarının birlikte
uygulanması.

### Oturum Güvenliği

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/oturum-guvenligi

Girişte oturum kimliğinin yenilenmesi, oturum sabitlemenin gözlenebilir sonucu, eşzamanlı oturumların listelenip tek tek sonlandırılması, boşta kalma ile mutlak ömür sınırlarının birlikte uygulanması ve çerez niteliklerinin rolü.

Önceki dersin son kuralı, başarılı bir sıfırlamanın hesaba ait tüm oturumları kapatmasıydı.
O cümlenin karşılığı, oturumun sunucu tarafında bir kayıt olmasıdır: kapatılabilen şey,
kaydı tutulan şeydir. Bu ders oturumun açılışından sonlandırılmasına kadar geçen ömrünü ele
alıyor.

Oturum Tabanlı Kimlik Doğrulama dersinde oturumun ne olduğu kurulmuştu: sunucu bir kayıt
tutar, kaydın kimliğini tarayıcıya çerezle verir, sonraki isteklerde o kimliği görünce
kullanıcıyı tanır. Buradaki sorular kimliğin ne zaman değiştiği, ne kadar yaşadığı ve kimin
kapatabildiğidir.

#### Oturum Sabitleme ve Girişte Yenileme

Kullanıcı siteye girmeden önce de bir oturuma sahip olabilir — dil tercihi, sepet, arama
geçmişi gibi durumlar anonim bir oturumda tutulur. Kullanıcı giriş yaptığında bu oturumun
ne olacağı bir karardır ve iki seçenek vardır: aynı kimlik girişli sayılır ya da kimlik
yenilenir.

Birinci seçenek **oturum sabitleme (session fixation)** adı verilen soruna açıktır.
Kullanıcının giriş öncesindeki oturum kimliğini bilen biri, kullanıcı giriş yaptığı anda
aynı kimliğe sahip olmaya devam eder ve artık girişli bir oturumun kimliğini elinde tutar.
Kimliğin nasıl öğrenildiği ayrı bir konudur; kurulumun kusuru, öğrenilmiş bir kimliğin
giriş sonrasında da geçerli kalmasıdır.

Aşağıdaki blok iki kurulumu yerel bir sunucuda yan yana çalıştırıyor.

```bash
cat > oturum-sunucu.mjs <<'EOF'
import { createServer } from "node:http";
import { randomBytes } from "node:crypto";

const oturumlar = new Map();                  // kimlik -> { uye }

const kimlikOku = (istek) =>
  /(?:^|;\s*)oturum=([^;]+)/.exec(istek.headers.cookie ?? "")?.[1];

function yeniOturum(uye = null) {
  const kimlik = randomBytes(24).toString("base64url");
  oturumlar.set(kimlik, { uye });
  return kimlik;
}

const cerez = (kimlik) => `oturum=${kimlik}; Path=/; HttpOnly; SameSite=Lax`;

createServer((istek, yanit) => {
  const yol = new URL(istek.url, "http://yerel").pathname;
  const kimlik = kimlikOku(istek);

  if (yol === "/saglik") return yanit.writeHead(200).end("hazir\n");

  if (yol === "/") {                          // anonim ziyaret: oturum acilir
    if (kimlik && oturumlar.has(kimlik)) return yanit.writeHead(200).end(kimlik + "\n");
    const yeni = yeniOturum();
    return yanit.writeHead(200, { "set-cookie": cerez(yeni) }).end(yeni + "\n");
  }

  if (yol === "/kusurlu/giris") {              // reddedilmesi gereken kurulum
    const o = oturumlar.get(kimlik);
    if (!o) return yanit.writeHead(400).end("oturum yok\n");
    o.uye = "U-1001";                          // ayni kimlik, artik girisli
    return yanit.writeHead(200).end(kimlik + "\n");
  }

  if (yol === "/giris") {                      // dogru kurulum
    if (kimlik) oturumlar.delete(kimlik);      // eski kimlik gecersizlesir
    const yeni = yeniOturum("U-1001");
    return yanit.writeHead(200, { "set-cookie": cerez(yeni) }).end(yeni + "\n");
  }

  if (yol === "/ben") {
    const o = oturumlar.get(kimlik);
    return yanit.writeHead(o ? 200 : 401).end((o ? (o.uye ?? "anonim") : "gecersiz") + "\n");
  }

  yanit.writeHead(404).end("");
}).listen(8495, "127.0.0.1");
EOF
node oturum-sunucu.mjs & sunucu=$!
until curl -sf http://127.0.0.1:8495/saglik > /dev/null; do :; done

dene() {
  kavanoz=$(mktemp); yol=$1
  once=$(curl -s -c "$kavanoz" -b "$kavanoz" http://127.0.0.1:8495/)
  sonra=$(curl -s -c "$kavanoz" -b "$kavanoz" "http://127.0.0.1:8495$yol")
  echo "  giris oncesi kimlik : ${once:0:14}..."
  echo "  giris sonrasi kimlik: ${sonra:0:14}..."
  echo "  kimlik degisti mi   : $([ "$once" = "$sonra" ] && echo hayir || echo evet)"
  echo "  eski kimlikle /ben  : $(curl -s -H "cookie: oturum=$once" http://127.0.0.1:8495/ben)"
  rm -f "$kavanoz"
}

echo "--- reddedilmesi gereken kurulum (/kusurlu/giris) ---"; dene /kusurlu/giris
echo "--- dogru kurulum (/giris) ---";                        dene /giris

kill $sunucu
rm -f oturum-sunucu.mjs
```

```text
--- reddedilmesi gereken kurulum (/kusurlu/giris) ---
  giris oncesi kimlik : BpWfAGGoOo1Zvs...
  giris sonrasi kimlik: BpWfAGGoOo1Zvs...
  kimlik degisti mi   : hayir
  eski kimlikle /ben  : U-1001
--- dogru kurulum (/giris) ---
  giris oncesi kimlik : qmM42rssGO2qEm...
  giris sonrasi kimlik: w1CarutBqemvD1...
  kimlik degisti mi   : evet
  eski kimlikle /ben  : gecersiz
```

Kimlik değerleri her çalıştırmada değişir; iki kurulumun farkı değişmez. Reddedilmesi
gereken kurulumda giriş öncesinde ele geçmiş bir kimlik, giriş sonrasında U-1001 olarak
yanıt alıyor. Doğru kurulumda aynı kimlik `gecersiz` dönüyor, çünkü giriş anında eski
kayıt silinip yenisi açılıyor.

Kuralın yazılışı tek cümledir: **yetki düzeyi değiştiren her işlemde oturum kimliği
yenilenir.** Giriş bunun en belirgin örneğidir; parola değişikliği, ikinci etmenin
tamamlanması ve rol yükselmesi de aynı kuralın kapsamındadır. Yenileme sırasında anonim
oturumdaki taşınması gereken veriler yeni kayda kopyalanır — dil tercihi gibi. Taşınmayan
şey kimliğin kendisidir.

#### Eşzamanlı Oturumlar

Bir üye aynı anda birden çok yerden girmiş olabilir: şubedeki masaüstünden, telefonundan,
salon terminalinden. Bunların her biri ayrı bir oturum kaydıdır ve ayrı ayrı
sonlandırılabilir olmalıdır. Kullanıcıya gösterilen "açık oturumlar" listesi, hesabında
beklenmedik bir erişim olup olmadığını görebildiği tek yerdir.

Kayıt, oturum kimliğinin kendisini değil karmasını tutar. Gerekçe önceki dersteki sıfırlama
belirteciyle aynıdır: veritabanını okuyabilen biri, açık oturumları kullanabilecek durumda
olmamalıdır.

```bash
cat > oturum-depo.mjs <<'EOF'
import { randomBytes, createHash } from "node:crypto";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync(":memory:");
db.exec(`CREATE TABLE oturum(
  karma TEXT PRIMARY KEY, uye TEXT NOT NULL, aygit TEXT NOT NULL,
  acilis INTEGER NOT NULL, son_erisim INTEGER NOT NULL)`);

const karmala = (k) => createHash("sha256").update(k).digest("hex");
const BOSTA_MS = 30 * 60 * 1000;              // bosta kalma siniri: 30 dakika
const MUTLAK_MS = 12 * 60 * 60 * 1000;        // mutlak omur: 12 saat
const T0 = Date.parse("2026-03-10T09:00:00Z");   // ornegin "simdi"si

function ac(uye, aygit, acilisFark, erisimFark) {
  const kimlik = randomBytes(24).toString("base64url");
  db.prepare("INSERT INTO oturum VALUES (?,?,?,?,?)")
    .run(karmala(kimlik), uye, aygit, T0 - acilisFark, T0 - erisimFark);
  return kimlik;
}

const dk = 60 * 1000, sa = 60 * dk;
ac("U-1001", "masaustu-sube-1", 2 * sa, 3 * dk);
const telefon = ac("U-1001", "telefon", 1 * sa, 45 * dk);
const eski    = ac("U-1001", "salon-terminali", 14 * sa, 5 * dk);
ac("U-1002", "telefon", 30 * dk, 1 * dk);

const durum = `CASE
  WHEN ? - son_erisim > ${BOSTA_MS} THEN 'bosta doldu'
  WHEN ? - acilis     > ${MUTLAK_MS} THEN 'mutlak omur doldu'
  ELSE 'gecerli' END`;

const listele = (uye) => db.prepare(
  `SELECT aygit, (? - acilis)/60000 AS acilis_dk, (? - son_erisim)/60000 AS bosta_dk,
          ${durum} AS durum
   FROM oturum WHERE uye = ? ORDER BY acilis`).all(T0, T0, T0, T0, uye);

const yaz = (baslik, uye) => {
  console.log(baslik);
  for (const s of listele(uye)) {
    console.log(`  ${s.aygit.padEnd(16)} acilis=${String(s.acilis_dk).padStart(3)} dk  ` +
      `bosta=${String(s.bosta_dk).padStart(2)} dk  ${s.durum}`);
  }
};

yaz("U-1001 oturumlari:", "U-1001");

const silinen = db.prepare("DELETE FROM oturum WHERE karma = ? AND uye = ?")
  .run(karmala(eski), "U-1001").changes;
console.log(`\ntek oturum sonlandirildi (etkilenen satir: ${silinen})`);
yaz("U-1001 oturumlari:", "U-1001");

const hepsi = db.prepare("DELETE FROM oturum WHERE uye = ?").run("U-1001").changes;
console.log(`\nsifirlama sonrasi tum oturumlar kapatildi (etkilenen satir: ${hepsi})`);
console.log("U-1001 kalan oturum :",
  db.prepare("SELECT COUNT(*) s FROM oturum WHERE uye='U-1001'").get().s);
console.log("U-1002 kalan oturum :",
  db.prepare("SELECT COUNT(*) s FROM oturum WHERE uye='U-1002'").get().s);
console.log("telefon kimligi hala gecerli mi:",
  db.prepare("SELECT COUNT(*) s FROM oturum WHERE karma = ?").get(karmala(telefon)).s > 0);
EOF
node oturum-depo.mjs
```

```text
U-1001 oturumlari:
  salon-terminali  acilis=840 dk  bosta= 5 dk  mutlak omur doldu
  masaustu-sube-1  acilis=120 dk  bosta= 3 dk  gecerli
  telefon          acilis= 60 dk  bosta=45 dk  bosta doldu

tek oturum sonlandirildi (etkilenen satir: 1)
U-1001 oturumlari:
  masaustu-sube-1  acilis=120 dk  bosta= 3 dk  gecerli
  telefon          acilis= 60 dk  bosta=45 dk  bosta doldu

sifirlama sonrasi tum oturumlar kapatildi (etkilenen satir: 2)
U-1001 kalan oturum : 0
U-1002 kalan oturum : 1
telefon kimligi hala gecerli mi: false
```

Silme deyimindeki `AND uye = ?` koşulu, sonraki konunun tamamının habercisidir: kullanıcı
yalnız kendi oturumunu kapatabilmelidir. Koşul olmadan, kimliği bilen herkes herhangi bir
oturumu kapatabilir. Bu, nesne düzeyinde erişim denetiminin bu dersteki ilk görünümüdür.

Son üç satır sıfırlama kuralının kanıtıdır: U-1001'in tüm oturumları kapandı, U-1002'nin
oturumu etkilenmedi ve daha önce elde tutulan telefon kimliği artık kayıtta yok.

#### İki Ömür Sınırı

Çıktının durum sütunu iki ayrı sınırı birlikte değerlendiriyor.

**Boşta kalma sınırı**, son erişimden bu yana geçen süreyi ölçer. Amacı, kullanıcının başında
olmadığı bir aygıtta oturumun açık kalmasını engellemektir. Telefon oturumu bu sınıra takıldı:
kırk beş dakikadır kullanılmıyor.

**Mutlak ömür**, oturumun açılışından bu yana geçen süreyi ölçer ve kullanım ne olursa olsun
uygulanır. Amacı, sürekli kullanılan bir oturumun süresiz yaşamasını engellemektir. Salon
terminalindeki oturum bu sınıra takıldı: beş dakika önce kullanılmış olmasına karşın on dört
saattir açık.

İkisi birlikte bulunur çünkü ayrı sorunları çözer. Yalnız boşta kalma sınırı olan bir
kurulumda, ele geçirilmiş bir oturum düzenli isteklerle sonsuza kadar tazelenebilir. Yalnız
mutlak ömür olan bir kurulumda, terk edilmiş bir aygıttaki oturum sınıra kadar açık kalır.

Değerlerin seçimi hesabın yetkisine göre değişir. Ödünç masasındaki personel oturumu için
kısa boşta kalma sınırı, üye oturumu için daha uzun bir sınır seçilebilir. Ölçüt, oturumun
açabildiği kapının genişliğidir.

Sınırların uygulanması bir sorguda yapılır; ayrı bir temizleme görevi ise süresi dolmuş
kayıtları siler. İkisi birbirinin yerine geçmez: temizleme görevi gecikirse sorgu yine
doğru karar verir, sorgu gecikmeli çalışırsa temizleme kaydı yine de kaldırır.

#### Kimliğin Taşınması

Oturum kimliği tarayıcıyla sunucu arasında çerezle taşınır ve çerezin nitelikleri kimliğin
korunmasının bir parçasıdır. Yukarıdaki sunucu üç nitelik yazıyor.

`HttpOnly`, çerezin sayfadaki betiklerden okunmasını engeller. İstemci Güvenliği konusunda
ele alınan siteler arası betik çalıştırma bir sayfada gerçekleştiğinde, bu nitelik oturum
kimliğinin doğrudan okunmasının önüne geçer.

`SameSite`, çerezin başka sitelerden başlatılan isteklere eklenmesini sınırlar ve siteler
arası istek sahteciliğine karşı ilk savunmadır.

`Secure` niteliği yukarıdaki örnekte yoktur, çünkü yerel sunucu düz HTTP konuşuyor. Gerçek
bir dağıtımda bu nitelik zorunludur: çerez yalnız güvenli taşıma üzerinden gönderilir.
Taşıma güvenliği olmadan diğer iki niteliğin değeri sınırlıdır — kimlik ağ üzerinde açık
gider.

Bu üç nitelik, kimliğin başkasının eline geçmesini — **oturumun ele geçirilmesini (session
hijacking)** — zorlaştırır ama tümüyle engellemez. Bu yüzden sunucu tarafında ikinci bir
katman bulunur: oturum kaydında aygıt ve kaynak bilgisi tutulur, beklenmedik bir değişimde
oturum sonlandırılıp yeniden kimlik doğrulama istenir. Bu denetimin sıkılığı bir
ödünleşimdir; ağ değiştiren bir kullanıcıyı sürekli dışarı atan bir kural, kullanıcıyı
oturumu hiç kapatmamaya iter.

#### Özet

- Yetki düzeyi değiştiren her işlemde oturum kimliği yenilenir; yenilenmeyen kurulumda giriş
  öncesi bilinen bir kimlik giriş sonrasında da geçerli kalır.
- Oturumlar sunucu tarafında kayıt olarak tutulur ve kimliğin karması saklanır; kapatılabilen
  şey kaydı tutulan şeydir.
- Bir hesabın eşzamanlı oturumları listelenebilir, tek tek kapatılabilir ve sıfırlama
  sırasında toplu olarak sonlandırılabilir.
- Boşta kalma sınırı ile mutlak ömür ayrı sorunları çözer ve birlikte uygulanır.
- Çerez nitelikleri kimliğin taşınmasını korur; sunucu tarafındaki aygıt denetimi ikinci
  katmandır ve sıkılığı kullanıcı davranışıyla dengelenir.

#### Sonraki Adım

Bu konu boyunca soru tekti: sunucu, karşısındakinin kim olduğunu nasıl güvenle biliyor?
Oturum silme sorgusundaki `AND uye = ?` koşulu ise başka bir soruyu işaret etti — kim
olduğu bilinen birinin neyi yapabileceği. Sonraki konu bu ikinci soruya geçiyor ve rol
tabanlı erişim denetimiyle başlıyor: izinlerin kişiye değil göreve bağlanması, rol
devralmasının etkin izin kümesini nasıl gizlediği ve kütüphane personelinin yetkilerinin
bir çizelgeden nasıl türetildiği.

## Yetkilendirme Modelleri

Rol, öznitelik, ilişki ve politika temelli erişim denetimi modellerinin aynı senaryo kümesinde karşılaştırılması, belirteç kapsamlarının uç noktalara eşlenmesi, kiracı yalıtımı ve nesne düzeyinde erişim denetimi.

### Rol Tabanlı Erişim Denetimi

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/rol-tabanli-erisim-denetimi

İzinlerin kişiye değil göreve bağlanması, rol devralmasının etkin izin kümesini nasıl gizlediği, ortak senaryo kümesinde rol tabanlı kararın nerede doğru nerede yanlış olduğu ve rol patlamasının ölçülmesi.

Önceki konu "karşımdaki kim" sorusuna verilen yanıtları kurdu. Oturum silme sorgusundaki
`AND uye = ?` koşulu ise ikinci soruyu açtı: kim olduğu bilinen birinin neyi yapabileceği.
Bu konu o soruyla ilgili ve dört ayrı modelle yanıtlıyor. Modeller birbirinin yerine geçmez;
her biri belirli bir kısıt türünü ifade edebilir, başka bir türü edemez.

Karşılaştırmanın anlamlı olması için dört model **aynı senaryo kümesini** değerlendirecek.
Küme kütüphane ödünç servisinden alınmış sekiz istekten oluşuyor ve her senaryonun bir de
doğru yanıtı var — kütüphanenin iş kuralının verdiği yanıt. Modellerin başarısı, kendi
kararlarının bu yanıtla uyuşmasıyla ölçülüyor.

#### Rol, İzin ve Devralma

**Rol tabanlı erişim denetimi (role-based access control)**, izinleri kişilere değil
görevlere bağlar. Bu model İlişkisel Veritabanı Yönetimi kursunda veritabanı hesapları için
kurulmuştu; buradaki fark katmandır. Orada denetlenen şey bir bağlantının hangi tabloya
dokunabildiğiydi; burada denetlenen şey bir isteğin hangi işlemi yürütebildiğidir. İki
katman ayrı ayrı bulunur ve biri diğerinin yerine geçmez: uygulama katmanındaki denetim
atlanırsa veritabanı katmanı son savunmadır.

**İzin (permission)**, bir eylem ile bir kaynak türünün birleşimidir: `goruntule:odunc`,
`sil:ceza`. **Rol**, izinlerin adlandırılmış bir kümesidir. **Rol devralması (role
inheritance)** bir rolün başka bir rolün izinlerini kapsamasıdır; şube yöneticisi görevlinin
her şeyini yapabilsin diye görevli rolünü devralır.

Devralmanın bedeli şudur: bir rolün gerçek izin kümesi yazıldığı yerde görünmez, devralma
zincirinin kapanışı alınarak hesaplanır. Aşağıdaki betik bu kapanışı üretiyor ve ortak
senaryo kümesini değerlendiriyor.

```bash
cat > rbac.mjs <<'EOF'
const ROL_IZIN = {
  uye:             ["goruntule:odunc", "goruntule:liste"],
  gorevli:         ["guncelle:odunc", "olustur:odunc"],
  sube_yoneticisi: ["sil:ceza", "sil:odunc"],
};
const ROL_DEVRAL = { uye: [], gorevli: ["uye"], sube_yoneticisi: ["gorevli"] };
const OZNE_ROL = {
  "U-1001": ["uye"], "U-1002": ["uye"], "U-1005": ["uye"],
  "P-2001": ["gorevli"], "Y-3001": ["sube_yoneticisi"],
};

// Ortak senaryo kumesi: bu konudaki dort model ayni kumeyi degerlendirir.
// [ad, ozne, eylem, nesne, nesne ozellikleri, saat, dogru yanit]
const SENARYOLAR = [
  ["S1", "U-1001", "goruntule", "odunc:501",  { sahip: "U-1001", sube: "S-02" }, "10:15", true],
  ["S2", "U-1001", "goruntule", "odunc:502",  { sahip: "U-1002", sube: "S-02" }, "10:16", false],
  ["S3", "P-2001", "goruntule", "odunc:502",  { sahip: "U-1002", sube: "S-02" }, "10:20", true],
  ["S4", "P-2001", "goruntule", "odunc:503",  { sahip: "U-1005", sube: "S-01" }, "10:21", false],
  ["S5", "P-2001", "guncelle",  "odunc:502",  { sahip: "U-1002", sube: "S-02" }, "22:40", false],
  ["S6", "Y-3001", "sil",       "ceza:77",    { sahip: "U-1002", sube: "S-02" }, "11:05", true],
  ["S7", "U-1002", "goruntule", "liste:9",    { sahip: "U-1001", sube: "S-02" }, "12:00", true],
  ["S8", "U-1005", "goruntule", "liste:9",    { sahip: "U-1001", sube: "S-02" }, "12:01", false],
];

function etkinIzinler(rol, gorulen = new Set()) {
  if (gorulen.has(rol)) return new Set();          // devralma dongusune karsi
  gorulen.add(rol);
  const kume = new Set(ROL_IZIN[rol] ?? []);
  for (const ust of ROL_DEVRAL[rol] ?? []) {
    for (const izin of etkinIzinler(ust, gorulen)) kume.add(izin);
  }
  return kume;
}

const ozneIzinleri = (ozne) => {
  const k = new Set();
  for (const rol of OZNE_ROL[ozne] ?? []) for (const i of etkinIzinler(rol)) k.add(i);
  return k;
};

const rbac = (ozne, eylem, nesne) =>
  ozneIzinleri(ozne).has(`${eylem}:${nesne.split(":")[0]}`);

console.log("rol             | etkin izin kumesi");
console.log("----------------|-----------------------------------------------------");
for (const rol of Object.keys(ROL_IZIN)) {
  console.log(rol.padEnd(15) + " | " + [...etkinIzinler(rol)].sort().join(", "));
}

console.log("\nsenaryo | ozne   | eylem     | nesne      | RBAC   | dogru  | uyum");
console.log("--------|--------|-----------|------------|--------|--------|------");
let uyan = 0;
for (const [ad, ozne, eylem, nesne, , , dogru] of SENARYOLAR) {
  const karar = rbac(ozne, eylem, nesne);
  if (karar === dogru) uyan++;
  console.log(
    `${ad}      | ${ozne} | ${eylem.padEnd(9)} | ${nesne.padEnd(10)} | ` +
    `${(karar ? "izin" : "ret").padEnd(6)} | ${(dogru ? "izin" : "ret").padEnd(6)} | ` +
    (karar === dogru ? "+" : "-")
  );
}
console.log(`\nRBAC dogru karar: ${uyan}/${SENARYOLAR.length}`);
EOF
node rbac.mjs
```

```text
rol             | etkin izin kumesi
----------------|-----------------------------------------------------
uye             | goruntule:liste, goruntule:odunc
gorevli         | goruntule:liste, goruntule:odunc, guncelle:odunc, olustur:odunc
sube_yoneticisi | goruntule:liste, goruntule:odunc, guncelle:odunc, olustur:odunc, sil:ceza, sil:odunc

senaryo | ozne   | eylem     | nesne      | RBAC   | dogru  | uyum
--------|--------|-----------|------------|--------|--------|------
S1      | U-1001 | goruntule | odunc:501  | izin   | izin   | +
S2      | U-1001 | goruntule | odunc:502  | izin   | ret    | -
S3      | P-2001 | goruntule | odunc:502  | izin   | izin   | +
S4      | P-2001 | goruntule | odunc:503  | izin   | ret    | -
S5      | P-2001 | guncelle  | odunc:502  | izin   | ret    | -
S6      | Y-3001 | sil       | ceza:77    | izin   | izin   | +
S7      | U-1002 | goruntule | liste:9    | izin   | izin   | +
S8      | U-1005 | goruntule | liste:9    | izin   | ret    | -

RBAC dogru karar: 4/8
```

#### Modelin Doğru ve Yanlış Kararları

Sekiz senaryonun dördünde karar doğru, dördünde yanlış. Yanlışların hepsi aynı yöndedir:
model gereğinden fazla izin veriyor. Bu yön rastlantı değildir ve modelin yapısından gelir.

S2 en açık olanıdır. U-1001 kendi ödünç kaydını görüntüleyebilir, ama başka bir üyenin
kaydını göremez. Rol tabanlı model bu ikisini ayırt edemez, çünkü kararı yalnız özne ve
kaynak *türü* üzerinden verir. `odunc:501` ile `odunc:502` aynı türdendir; hangisinin kime
ait olduğu modelin göremediği bir bilgidir.

S4 aynı sorunun personel tarafıdır. Görevli ödünç kayıtlarını görüntüleyebilir; hangi
şubenin kayıtlarını görüntüleyebileceği izin adında yazılı değildir.

S5 zamanla ilgilidir. Ödünç kaydının mesai saatleri dışında değiştirilmemesi bir iş
kuralıdır ve rol tabanlı modelde ifade edilecek bir yeri yoktur — izinler isteğin bağlamını
görmez.

S8, S7 ile birlikte okunmalıdır. İkisinde de model "izin" diyor; S7'de bu doğru, S8'de
yanlış. Doğru olanı da yanlış nedenle doğrudur: model, listenin U-1002 ile paylaşılmış olup
U-1005 ile paylaşılmamış olduğunu bilmiyor, yalnız her üyenin liste görüntüleyebildiğini
biliyor. Doğru kararın doğru nedenle verilmesi, sonraki iki dersin konusu.

Bu dörtlünün ortak adı vardır: rol tabanlı model **kaba taneli** bir modeldir. Kaynağın
türüne kadar iner, örneğine inemez.

#### Rol Patlaması

Eksik ayrımları rol tabanlı model içinde çözmenin bir yolu vardır: eksik olan koşulu rolün
adına yazmak. Şube ayrımı için `gorevli_S01`, `gorevli_S02`; mesai ayrımı için bunların
mesai içi ve mesai dışı sürümleri. Bu yolun maliyeti hesaplanabilir.

```bash
cat > rol-patlamasi.mjs <<'EOF'
const boyutlar = [
  ["gorev", 4, "okuyucu, gorevli, katalogcu, sube yoneticisi"],
  ["sube", 12, "kutuphanenin sube sayisi"],
  ["mesai", 2, "mesai ici / mesai disi"],
  ["kayit sahipligi", 2, "kendi kaydi / baskasinin kaydi"],
];

console.log("eklenen kosul       | carpan | gereken rol sayisi | ornek");
console.log("--------------------|--------|--------------------|--------------------------------");
let rol = 1;
for (const [ad, carpan, ornek] of boyutlar) {
  rol *= carpan;
  console.log(ad.padEnd(19) + " | " + String(carpan).padStart(6) + " | " +
    String(rol).padStart(18) + " | " + ornek);
}
console.log(`\nrol adlarinin ornegi: gorevli_S02_mesai_ici_baskasinin_kaydi`);
console.log(`atama sayisi: her personel icin ${rol} rolden dogru olani secilir`);
EOF
node rol-patlamasi.mjs
```

```text
eklenen kosul       | carpan | gereken rol sayisi | ornek
--------------------|--------|--------------------|--------------------------------
gorev               |      4 |                  4 | okuyucu, gorevli, katalogcu, sube yoneticisi
sube                |     12 |                 48 | kutuphanenin sube sayisi
mesai               |      2 |                 96 | mesai ici / mesai disi
kayit sahipligi     |      2 |                192 | kendi kaydi / baskasinin kaydi

rol adlarinin ornegi: gorevli_S02_mesai_ici_baskasinin_kaydi
atama sayisi: her personel icin 192 rolden dogru olani secilir
```

Dört görev, bir şube eklendiğinde kırk sekiz role çıkıyor; iki koşul daha eklendiğinde yüz
doksan iki. Sayının kendisinden çok, büyüme biçimi önemlidir: her yeni koşul rol sayısını
**çarpar**. On üçüncü şube açıldığında on altı yeni rol tanımlanması gerekir ve bunların
her biri elle bakılan bir kayıttır.

İkinci maliyet okunabilirliktir. `gorevli_S02_mesai_ici_baskasinin_kaydi` adlı bir rolün ne
anlama geldiği adından çıkarılabilir, ama böyle yüz doksan iki adın arasında bir yanlışın
fark edilmesi çıkarılamaz. Bir personelin yanlış role atanması, yetki çizelgesine bakarak
görülemez hâle gelir.

Buradaki sınır rol tabanlı modelin kusuru değil, kapsamıdır. Model, görevin sabit olduğu ve
kararın isteğin bağlamına bağlı olmadığı durumlarda yalın ve denetlenebilirdir.

#### Modelin Doğru Kullanıldığı Yer

Rol tabanlı modelin bırakılması gerekmiyor; sınırının bilinmesi gerekiyor. Sağlam kurulum
şu ayrımı yapar.

**Kaba taneli karar rolle verilir.** "Bu istek türü bu göreve açık mı?" sorusunun yanıtı
roldedir. Personel olmayan birinin ödünç kaydı güncelleme uç noktasına hiç ulaşmaması, rol
denetiminin işidir ve isteğin en başında yapılır.

**İnce taneli karar ayrı bir katmanda verilir.** "Bu belirli kayıt bu kişiye açık mı?"
sorusu, sonraki derslerin konusudur ve rolle değil, kaydın kendisiyle ilgilidir.

**Roller görevden türetilir, kişiden değil.** Bir role "şu kişi de erişebilsin" diye izin
eklemek, rolün adını yalanlar. Gerek varsa yeni bir rol tanımlanır.

**Rol atamaları düzenli denetlenir.** Bir kişinin taşıdığı etkin izin kümesi ile görevinin
gerektirdiği küme arasındaki fark ölçülür. Bu denetimin veritabanı tarafındaki karşılığı
İlişkisel Veritabanı Yönetimi kursunda kurulmuştu; uygulama tarafında da aynı yordam
uygulanır.

#### Özet

- Rol tabanlı erişim denetimi izinleri göreve bağlar; izin, bir eylem ile bir kaynak
  türünün birleşimidir.
- Rol devralması etkin izin kümesini yazıldığı yerde görünmez kılar; küme, devralma
  zincirinin kapanışı alınarak hesaplanır.
- Model kaba tanelidir: kaynağın türüne iner, örneğine inemez. Ortak senaryo kümesinde
  sekiz karardan dördü yanlış çıkmıştır ve dördü de gereğinden fazla izin yönündedir.
- Eksik koşulları rol adına yazmak rol sayısını her koşulda çarpar; on iki şube ve iki
  koşul yüz doksan iki rol üretir.
- Rol tabanlı karar isteğin başında kaba taneli süzgeç olarak kalır; kayıt düzeyindeki
  karar ayrı bir katmanda verilir.

#### Sonraki Adım

Yanlış çıkan dört kararın üçü aynı eksikten geliyor: model, isteğin bağlamını görmüyor.
Kaydın sahibi kim, personel hangi şubede çalışıyor, istek hangi saatte geldi — bunların
hepsi karar anında bilinen ama izin adında yeri olmayan bilgiler. Sonraki ders bu bilgileri
kararın girdisi hâline getiren öznitelik tabanlı erişim denetimini ele alıyor ve aynı sekiz
senaryoyu bu modelle yeniden değerlendiriyor.

### Öznitelik Tabanlı Erişim Denetimi

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/oznitelik-tabanli-erisim-denetimi

Kararın özne, nesne, eylem ve bağlam öznitelikleriyle verilmesi, ret önceliğinin kural birleştirmedeki rolü, aynı senaryo kümesinde öznitelik tabanlı modelin doğru ve yanlış kararları ve özniteliğin nereden okunduğunun sonucu.

Önceki dersin sekiz senaryosunda dört karar yanlış çıktı ve üçünün nedeni aynıydı: rol
tabanlı model isteğin bağlamını göremiyor. Kaydın sahibinin kim olduğu, personelin hangi
şubede çalıştığı, isteğin hangi saatte geldiği — bunların hepsi karar anında bilinen
bilgilerdir, ama izin adının içinde onlara yer yoktur.

**Öznitelik tabanlı erişim denetimi (attribute-based access control)** bu bilgileri kararın
girdisi hâline getirir. Kararı veren şey artık bir izin listesindeki üyelik değil, öznitelik
değerleri üzerinde çalışan bir kuraldır.

#### Dört Öznitelik Kümesi

Model, karar anında dört kaynaktan öznitelik toplar.

**Özne öznitelikleri**, isteği yapanın kendisine aittir: kod, rol, çalıştığı şube, kıdem,
kimlik doğrulamanın hangi etmenlerle yapıldığı.

**Nesne öznitelikleri**, erişilmek istenen kayda aittir: türü, sahibi, ait olduğu şube,
gizlilik sınıfı, oluşturulma tarihi.

**Eylem**, yapılmak istenen işlemdir; rol tabanlı modeldekiyle aynı anlamı taşır.

**Bağlam öznitelikleri** ne özneye ne nesneye aittir; isteğin kendisine aittir: saat, ağ
konumu, isteğin geldiği aygıt, oturumun yaşı.

Kural, bu dört kümenin değerleri üzerinde bir koşuldur. "Kendi kaydını görüntüleyebilir"
kuralı, özne kodunun nesne sahibine eşitliğidir; şube kuralı iki şube özniteliğinin
eşitliğidir. Rol tabanlı modelde ifade edilemeyen şey bu eşitliklerdi, çünkü orada karar
kayda hiç bakmıyordu.

#### Kuralların Birleştirilmesi

Birden çok kural aynı isteğe uyabilir ve sonuçları çelişebilir. Birleştirme kuralı bu
durumda ne olacağını söyler. Yerleşik seçim **ret önceliğidir**: eşleşen kurallardan biri
ret diyorsa karar rettir, izin diyen kuralların sayısı önemsizdir. Hiçbir kural eşleşmezse
karar yine rettir — izin, açıkça verilmesi gereken bir şeydir.

Aşağıdaki değerlendirici dört kuralı bu birleştirmeyle uyguluyor ve önceki dersin sekiz
senaryosunu yeniden karara bağlıyor. Son sütun, kararı hangi kuralın verdiğini gösteriyor.

```bash
cat > abac.mjs <<'EOF'
const OZNE = {
  "U-1001": { rol: "uye", sube: "S-02" }, "U-1002": { rol: "uye", sube: "S-02" },
  "U-1005": { rol: "uye", sube: "S-01" }, "P-2001": { rol: "gorevli", sube: "S-02" },
  "Y-3001": { rol: "sube_yoneticisi", sube: "S-02" },
};

// Onceki dersteki ortak senaryo kumesi.
const SENARYOLAR = [
  ["S1", "U-1001", "goruntule", "odunc:501",  { sahip: "U-1001", sube: "S-02" }, "10:15", true],
  ["S2", "U-1001", "goruntule", "odunc:502",  { sahip: "U-1002", sube: "S-02" }, "10:16", false],
  ["S3", "P-2001", "goruntule", "odunc:502",  { sahip: "U-1002", sube: "S-02" }, "10:20", true],
  ["S4", "P-2001", "goruntule", "odunc:503",  { sahip: "U-1005", sube: "S-01" }, "10:21", false],
  ["S5", "P-2001", "guncelle",  "odunc:502",  { sahip: "U-1002", sube: "S-02" }, "22:40", false],
  ["S6", "Y-3001", "sil",       "ceza:77",    { sahip: "U-1002", sube: "S-02" }, "11:05", true],
  ["S7", "U-1002", "goruntule", "liste:9",    { sahip: "U-1001", sube: "S-02" }, "12:00", true],
  ["S8", "U-1005", "goruntule", "liste:9",    { sahip: "U-1001", sube: "S-02" }, "12:01", false],
];

const mesaiIci = (saat) => saat >= "08:30" && saat <= "18:00";
const YAZAN = new Set(["guncelle", "olustur", "sil"]);

const KURALLAR = [
  { ad: "K1 kendi kaydi", etki: "izin",
    kosul: (o, e, n) => e === "goruntule" && n.sahip === o.kod },
  { ad: "K2 sube gorevlisi", etki: "izin",
    kosul: (o, e, n) => ["gorevli", "sube_yoneticisi"].includes(o.rol) &&
      n.sube === o.sube && ["goruntule", "guncelle", "olustur"].includes(e) },
  { ad: "K3 ceza silme", etki: "izin",
    kosul: (o, e, n) => o.rol === "sube_yoneticisi" && n.sube === o.sube &&
      e === "sil" && n.tur === "ceza" },
  { ad: "K4 mesai disi yazma", etki: "ret",
    kosul: (o, e, n, b) => YAZAN.has(e) && !mesaiIci(b.saat) && o.rol !== "sube_yoneticisi" },
];

function abac(ozneKod, eylem, nesneAdi, nesneOzn, saat) {
  const o = { kod: ozneKod, ...OZNE[ozneKod] };
  const n = { tur: nesneAdi.split(":")[0], ...nesneOzn };
  const b = { saat };
  const eslesen = KURALLAR.filter((k) => k.kosul(o, eylem, n, b));
  const ret = eslesen.find((k) => k.etki === "ret");
  const izin = eslesen.find((k) => k.etki === "izin");
  if (ret) return [false, `${ret.ad} (ret onceliklidir)`];
  if (izin) return [true, izin.ad];
  return [false, "eslesen kural yok"];
}

console.log("senaryo | ABAC | dogru | uyum | karari veren kural");
console.log("--------|------|-------|------|----------------------------");
let uyan = 0;
for (const [ad, ozne, eylem, nesne, ozn, saat, dogru] of SENARYOLAR) {
  const [karar, gerekce] = abac(ozne, eylem, nesne, ozn, saat);
  if (karar === dogru) uyan++;
  console.log(`${ad}      | ${(karar ? "izin" : "ret").padEnd(4)} | ` +
    `${(dogru ? "izin" : "ret").padEnd(5)} | ${(karar === dogru ? "+" : "-").padEnd(4)} | ${gerekce}`);
}
console.log(`\nABAC dogru karar: ${uyan}/${SENARYOLAR.length}`);
EOF
node abac.mjs
```

```text
senaryo | ABAC | dogru | uyum | karari veren kural
--------|------|-------|------|----------------------------
S1      | izin | izin  | +    | K1 kendi kaydi
S2      | ret  | ret   | +    | eslesen kural yok
S3      | izin | izin  | +    | K2 sube gorevlisi
S4      | ret  | ret   | +    | eslesen kural yok
S5      | ret  | ret   | +    | K4 mesai disi yazma (ret onceliklidir)
S6      | izin | izin  | +    | K3 ceza silme
S7      | ret  | izin  | -    | eslesen kural yok
S8      | ret  | ret   | +    | eslesen kural yok

ABAC dogru karar: 7/8
```

Rol tabanlı modelde yanlış çıkan dört karardan üçü düzeldi. S2 ve S4, sahiplik ve şube
özniteliklerinin karşılaştırılmasıyla; S5, bağlam özniteliğiyle. S8'in kararı da artık
doğru nedenle veriliyor: liste U-1005'in değil, dolayısıyla hiçbir kural eşleşmiyor.

Gerekçe sütunu modelin ikinci kazancıdır. Her karar, onu veren kuralın adıyla birlikte
üretilebilir; "neden reddedildi" sorusunun yanıtı kütüğe yazılabilir. Rol tabanlı modelde
bu yanıt "rolde o izin yoktu" cümlesinden ibaretti.

#### Modelin Ulaşamadığı Yer

S7 hâlâ yanlış. U-1001, dokuz numaralı okuma listesini U-1002 ile paylaşmış; U-1002 bu
listeyi görebilmeli. Öznitelik tabanlı modelde bu kısıtı yazmanın yolu, paylaşımı bir
öznitelik hâline getirmektir: nesneye bir `paylasilanlar` alanı eklenir ve kural, özne
kodunun o listede olup olmadığına bakar.

Bu yol küçük ölçekte çalışır, iki yerde tıkanır. Birincisi geçişkenliktir: U-1002 listeyi
kendi çalışma grubuyla paylaşırsa, grubun üyeleri de erişmelidir. Öznitelik listesi bunu
ancak grubun tüm üyelerini tek tek içine alarak ifade eder ve grup değiştikçe her nesnenin
özniteliği güncellenmelidir. İkincisi dolaylılıktır: "şube yöneticisinin yönettiği şubede
çalışan personelin açtığı ödünç kayıtları" gibi bir kısıt, iki adım uzaklıktaki bir bağa
dayanır ve tek bir öznitelik alanına sığmaz.

Bu iki durum, kısıtın öznitelik değil **ilişki** olduğunun işaretidir. Sonraki dersin konusu
budur.

#### Özniteliğin Kaynağı ve Tazeliği

Öznitelik tabanlı modelin işletimdeki asıl sorusu kuralların nasıl yazıldığı değil,
özniteliklerin nereden okunduğudur. İki seçenek vardır: değerler kimlik belirtecinin içinde
taşınır ya da karar anında kaydından okunur.

Belirteçten okumak hızlıdır ve ek sorgu gerektirmez. Bedeli, değerin belirtecin üretildiği
andaki değer olmasıdır. Aşağıdaki örnek, şube değiştiren bir görevlinin iki isteğini iki
kaynakla karara bağlıyor.

```bash
cat > tazelik.mjs <<'EOF'
const KAYIT = { "P-2001": { rol: "gorevli", sube: "S-01" } };   // 12:00'de S-02'den S-01'e gecti
const BELIRTEC = { kod: "P-2001", rol: "gorevli", sube: "S-02", uretim: "09:00" };

const kural = (o, n) => o.rol === "gorevli" && n.sube === o.sube;

const istekler = [
  ["odunc:502 (sube S-02)", { sube: "S-02" }, false],   // artik gormemeli
  ["odunc:503 (sube S-01)", { sube: "S-01" }, true],    // artik gormeli
];

console.log("oznitelik kaynagi        | " + istekler.map((i) => i[0]).join(" | "));
console.log("-------------------------|-----------------------|-----------------------");
for (const [ad, o] of [["belirtecten (09:00)", { ...BELIRTEC }],
                       ["kayittan (istek ani)", { kod: "P-2001", ...KAYIT["P-2001"] }]]) {
  const hucreler = istekler.map(([, n, dogru]) => {
    const karar = kural(o, n);
    return `${karar ? "izin" : "ret"} ${karar === dogru ? "(dogru)" : "(YANLIS)"}`.padEnd(21);
  });
  console.log((ad.padEnd(24) + " | " + hucreler.join(" | ")).trimEnd());
}
EOF
node tazelik.mjs
```

```text
oznitelik kaynagi        | odunc:502 (sube S-02) | odunc:503 (sube S-01)
-------------------------|-----------------------|-----------------------
belirtecten (09:00)      | izin (YANLIS)         | ret (YANLIS)
kayittan (istek ani)     | ret (dogru)           | izin (dogru)
```

Belirteçteki eski değer iki yönde birden yanlış karar üretiyor: görevlinin artık görmemesi
gereken kayda izin veriyor, görmesi gereken kaydı reddediyor. İkincisi bir destek talebi
olarak görünür ve düzeltilir; birincisi hiç görünmez.

Karar kuralı şudur: **kararı değiştiren öznitelik, değişebiliyorsa karar anında okunur.**
Belirteçte yalnız değişmeyen ya da belirteç ömrü içinde değişmesi kabul edilebilir
öznitelikler taşınır. Belirteç ömrü kısaldıkça bu ayrımın önemi azalır — yenileme
belirteçleri dersindeki ömür kararı, buradaki tazelik kararıyla aynı ödünleşimin iki
yüzüdür.

Kayıttan okumanın bedeli her istekte bir sorgudur. Bu maliyet, öznitelikleri istek başına
bir kez okuyup istek boyunca önbelleğe almakla sınırlanır; önbelleğin ömrü isteğin ömrünü
aşmamalıdır.

#### Kuralların Denetlenebilirliği

Öznitelik tabanlı modelin kazandırdığı ifade gücü, bir maliyetle gelir: kuralların bir
arada ne yaptığını görmek zorlaşır. Rol tabanlı modelde bir hesabın etkin izin kümesi
hesaplanabiliyordu; öznitelik kurallarında böyle bir küme yoktur, çünkü karar isteğin
kendisine bağlıdır.

Bunun yerine iki denetim yordamı kullanılır.

**Senaryo kümesi.** Bu dersteki tablonun kendisi bir denetim aracıdır: bilinen istekler ve
beklenen yanıtlar bir küme hâlinde tutulur, kural her değiştiğinde küme yeniden
çalıştırılır. Uyum sütununda eksi çıkması, kuralın metnine bakmayı gerektirir.

**Kural kapsamı.** Hiçbir senaryoda eşleşmeyen bir kural ya gereksizdir ya da yanlış
yazılmıştır; her senaryoda eşleşen bir kural büyük olasılıkla fazla geniştir. Kuralların
kaç kez eşleştiği sayılabilir ve bu sayı kuralın yazılışı hakkında bilgi verir.

Üçüncü bir kural yazım disiplinidir: kurallar yan etkisiz olmalı, aynı girdide her zaman
aynı kararı üretmelidir. Karar sırasında kayıt yazan, dış servis çağıran ya da rastgele
değer kullanan bir kural, yeniden çalıştırılamaz ve denetlenemez.

#### Özet

- Öznitelik tabanlı erişim denetimi kararı özne, nesne, eylem ve bağlam özniteliklerinden
  üretir; rol tabanlı modelde ifade edilemeyen sahiplik ve şube kısıtları burada eşitlik
  koşulu olarak yazılır.
- Kural birleştirmede ret önceliklidir ve hiçbir kural eşleşmezse karar rettir.
- Model her kararın gerekçesini üretebilir; bu, reddin nedeninin kütüğe yazılmasını mümkün
  kılar.
- Geçişken ve dolaylı bağlar öznitelik alanına sığmaz; bu kısıtlar öznitelik değil ilişki
  niteliğindedir.
- Kararı değiştiren öznitelik değişebiliyorsa karar anında okunur; belirteçte taşınan eski
  değer iki yönde birden yanlış karar üretir.

#### Sonraki Adım

S7 iki derstir yanlış karara bağlanıyor. Paylaşılmış bir listenin erişilebilirliği, ne
özneye ne nesneye ait bir özelliktir; ikisi arasındaki bağdır. Sonraki ders bu bağları
birinci sınıf veri hâline getiren ilişki tabanlı erişim denetimini ele alıyor: nesneler ve
özneler bir çizge oluşturuyor, erişim sorusu da o çizge üzerinde bir ulaşılabilirlik
sorgusuna dönüşüyor.

### İlişki Tabanlı Erişim Denetimi

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/iliski-tabanli-erisim-denetimi

Özneler ve nesneler arasındaki bağların birinci sınıf veri hâline gelmesi, erişim sorusunun çizge üzerinde bir ulaşılabilirlik sorgusuna dönüşmesi, geçişken erişim ve tek nesne denetimiyle listeleme sorgusunun maliyet farkı.

Önceki iki derste S7 yanlış karara bağlandı. U-1001, dokuz numaralı okuma listesini U-1002
ile paylaşmıştı; U-1002 bu listeyi görebilmeliydi. Ne rol tabanlı model ne öznitelik tabanlı
model bunu ifade edebildi, çünkü paylaşım ne öznenin ne nesnenin bir özelliğidir — ikisi
arasındaki bağdır.

**İlişki tabanlı erişim denetimi (relationship-based access control)** bu bağları birinci
sınıf veri hâline getirir. Sistemdeki özneler ve nesneler bir çizgenin düğümleri, aralarındaki
bağlar da adlandırılmış kenarlarıdır. Erişim sorusu, bu çizge üzerinde bir ulaşılabilirlik
sorgusuna dönüşür: özneden nesneye, belirli bir kenar dizisini izleyerek gidilebiliyor mu?

#### Çizge, Kenarlar ve Yol Desenleri

Kütüphane ödünç servisinin ilişkileri az sayıda kenar türüyle yazılabilir. `sahip`, bir üyeyi
kendi kaydına bağlar. `ait`, bir kaydı şubesine bağlar. `calisir` ve `yonetir`, personeli
şubesine bağlar. `paylasildi`, bir listeyi paylaşıldığı kişiye ya da gruba bağlar. `uyesi`,
bir kişiyi gruba bağlar.

Erişim kuralı, bu kenarlardan oluşan bir **yol desenidir**. "Kendi ödünç kaydını
görüntüleyebilir" kuralı tek adımlık bir desendir: `sahip`. "Şube görevlisi şubesinin
kayıtlarını görüntüleyebilir" kuralı iki adımlıdır ve ikinci adımda kenar ters yönde
izlenir: personelden şubeye `calisir`, şubeden kayda `ait` kenarının tersi.

Aşağıdaki değerlendirici bu desenleri yürütüyor. Her adımda bir düğüm kümesinden komşu
kümesine geçiliyor; bu, Veri Yapıları kursundaki enine aramanın kenar türüne göre süzülmüş
biçimidir.

```bash
cat > rebac.mjs <<'EOF'
const KENARLAR = [                      // [kaynak, iliski, hedef]
  ["U-1001", "sahip", "odunc:501"], ["U-1002", "sahip", "odunc:502"],
  ["U-1005", "sahip", "odunc:503"], ["U-1001", "sahip", "liste:9"],
  ["odunc:501", "ait", "S-02"], ["odunc:502", "ait", "S-02"],
  ["odunc:503", "ait", "S-01"], ["ceza:77", "ait", "S-02"],
  ["P-2001", "calisir", "S-02"], ["Y-3001", "yonetir", "S-02"],
  ["liste:9", "paylasildi", "U-1002"], ["liste:9", "paylasildi", "grup:okuma-kulubu"],
  ["U-1006", "uyesi", "grup:okuma-kulubu"],
];

// Desendeki "~iliski", kenarin ters yonde izlenmesi demektir.
const DESENLER = {
  "goruntule:odunc": [["sahip"], ["calisir", "~ait"], ["yonetir", "~ait"]],
  "guncelle:odunc":  [["calisir", "~ait"], ["yonetir", "~ait"]],
  "olustur:odunc":   [["calisir", "~ait"], ["yonetir", "~ait"]],
  "sil:odunc":       [["yonetir", "~ait"]],
  "sil:ceza":        [["yonetir", "~ait"]],
  "goruntule:liste": [["sahip"], ["~paylasildi"], ["uyesi", "~paylasildi"]],
};

function adim(dugumler, iliski) {
  const ters = iliski.startsWith("~");
  const ad = ters ? iliski.slice(1) : iliski;
  const sonuc = new Set();
  for (const [k, i, h] of KENARLAR) {
    if (i !== ad) continue;
    if (!ters && dugumler.has(k)) sonuc.add(h);
    if (ters && dugumler.has(h)) sonuc.add(k);
  }
  return sonuc;
}

function rebac(ozne, eylem, nesne) {
  for (const desen of DESENLER[`${eylem}:${nesne.split(":")[0]}`] ?? []) {
    let cephe = new Set([ozne]);
    for (const iliski of desen) cephe = adim(cephe, iliski);
    if (cephe.has(nesne)) return [true, desen.join(" -> ")];
  }
  return [false, "ulasilabilir yol yok"];
}

const SENARYOLAR = [
  ["S1", "U-1001", "goruntule", "odunc:501", true],
  ["S2", "U-1001", "goruntule", "odunc:502", false],
  ["S3", "P-2001", "goruntule", "odunc:502", true],
  ["S4", "P-2001", "goruntule", "odunc:503", false],
  ["S5", "P-2001", "guncelle",  "odunc:502", false],
  ["S6", "Y-3001", "sil",       "ceza:77",   true],
  ["S7", "U-1002", "goruntule", "liste:9",   true],
  ["S8", "U-1005", "goruntule", "liste:9",   false],
];

console.log("senaryo | ReBAC | dogru | uyum | bulunan yol");
console.log("--------|-------|-------|------|--------------------------");
let uyan = 0;
for (const [ad, ozne, eylem, nesne, dogru] of SENARYOLAR) {
  const [karar, yol] = rebac(ozne, eylem, nesne);
  if (karar === dogru) uyan++;
  console.log(`${ad}      | ${(karar ? "izin" : "ret").padEnd(5)} | ` +
    `${(dogru ? "izin" : "ret").padEnd(5)} | ${(karar === dogru ? "+" : "-").padEnd(4)} | ${yol}`);
}
console.log(`\nReBAC dogru karar: ${uyan}/${SENARYOLAR.length}`);

console.log("\ngecisken erisim (ortak kume disinda):");
for (const kod of ["U-1006", "U-1005"]) {
  const [karar, yol] = rebac(kod, "goruntule", "liste:9");
  console.log(`  ${kod} -> liste:9 : ${karar ? "izin" : "ret"}  (${yol})`);
}
EOF
node rebac.mjs
```

```text
senaryo | ReBAC | dogru | uyum | bulunan yol
--------|-------|-------|------|--------------------------
S1      | izin  | izin  | +    | sahip
S2      | ret   | ret   | +    | ulasilabilir yol yok
S3      | izin  | izin  | +    | calisir -> ~ait
S4      | ret   | ret   | +    | ulasilabilir yol yok
S5      | izin  | ret   | -    | calisir -> ~ait
S6      | izin  | izin  | +    | yonetir -> ~ait
S7      | izin  | izin  | +    | ~paylasildi
S8      | ret   | ret   | +    | ulasilabilir yol yok

ReBAC dogru karar: 7/8

gecisken erisim (ortak kume disinda):
  U-1006 -> liste:9 : izin  (uyesi -> ~paylasildi)
  U-1005 -> liste:9 : ret  (ulasilabilir yol yok)
```

S7 üç derste ilk kez doğru nedenle çözüldü: listeye U-1002'den bir `paylasildi` kenarı
gidiyor. S8 aynı desenle reddedildi, çünkü U-1005'e giden kenar yok.

Son iki satır modelin asıl gücünü gösteriyor. U-1006 listeye doğrudan bağlı değil; okuma
kulübü grubunun üyesi ve liste o grupla paylaşılmış. İki adımlık yol bu bağı kuruyor.
Öznitelik tabanlı modelde aynı sonuca ulaşmak için grubun tüm üyelerinin nesnenin öznitelik
alanına kopyalanması ve grup her değiştiğinde bu kopyanın güncellenmesi gerekirdi. Çizgede
grup üyeliği tek bir kenardır ve kenarın eklenmesi tüm listeleri etkiler.

#### Modelin Ulaşamadığı Yer

S5 bu modelde yanlış. Ödünç kaydının mesai dışında güncellenmemesi bir bağlam kısıtıdır ve
çizgede karşılığı yoktur — saat, iki düğüm arasındaki bir kenar değildir.

Bu, önceki dersin aynasıdır. Öznitelik tabanlı model bağlamı görüyor ama ilişkiyi
göremiyordu; ilişki tabanlı model ilişkiyi görüyor ama bağlamı göremiyor. İki modelin
eksikleri örtüşmüyor, tamamlıyor. Bu gözlem sonraki dersin çıkış noktası.

#### İki Ayrı Soru ve Maliyetleri

İlişki tabanlı modelde iki farklı soru sorulur ve maliyetleri aynı değildir.

Birincisi **denetimdir**: "bu özne bu nesneye erişebilir mi?" Yanıt bulunduğu anda arama
durur. İkincisi **listelemedir**: "bu öznenin erişebildiği nesneler hangileri?" Bu soru bir
liste sayfası çizmek için gerekir ve tüm desenlerin yürütülmesini gerektirir.

```bash
cat > rebac-maliyet.mjs <<'EOF'
const KENARLAR = [
  ["U-1001", "sahip", "odunc:501"], ["U-1002", "sahip", "odunc:502"],
  ["U-1005", "sahip", "odunc:503"], ["U-1001", "sahip", "liste:9"],
  ["odunc:501", "ait", "S-02"], ["odunc:502", "ait", "S-02"],
  ["odunc:503", "ait", "S-01"], ["ceza:77", "ait", "S-02"],
  ["P-2001", "calisir", "S-02"], ["Y-3001", "yonetir", "S-02"],
  ["liste:9", "paylasildi", "U-1002"], ["liste:9", "paylasildi", "grup:okuma-kulubu"],
  ["U-1006", "uyesi", "grup:okuma-kulubu"],
];
const DESENLER = {
  "goruntule:odunc": [["sahip"], ["calisir", "~ait"], ["yonetir", "~ait"]],
  "goruntule:liste": [["sahip"], ["~paylasildi"], ["uyesi", "~paylasildi"]],
};

let kenarOkuma = 0;
function adim(dugumler, iliski) {
  const ters = iliski.startsWith("~"), ad = ters ? iliski.slice(1) : iliski;
  const sonuc = new Set();
  for (const [k, i, h] of KENARLAR) {
    kenarOkuma++;
    if (i !== ad) continue;
    if (!ters && dugumler.has(k)) sonuc.add(h);
    if (ters && dugumler.has(h)) sonuc.add(k);
  }
  return sonuc;
}

const yuru = (ozne, desen) => {
  let cephe = new Set([ozne]);
  for (const iliski of desen) cephe = adim(cephe, iliski);
  return cephe;
};

function denetle(ozne, izin, nesne) {              // ilk eslesen desende durur
  for (const desen of DESENLER[izin] ?? []) if (yuru(ozne, desen).has(nesne)) return true;
  return false;
}

function erisilebilirler(ozne, izin) {             // tum desenler yurunur
  const hepsi = new Set();
  for (const desen of DESENLER[izin] ?? []) for (const d of yuru(ozne, desen)) hepsi.add(d);
  const tur = izin.split(":")[1] + ":";
  return [...hepsi].filter((d) => d.startsWith(tur)).sort();
}

console.log("tek nesne denetimi (ilk eslesen desende durur):");
for (const [ozne, nesne] of [["P-2001", "odunc:502"], ["U-1001", "odunc:501"],
                             ["U-1005", "odunc:502"]]) {
  kenarOkuma = 0;
  const k = denetle(ozne, "goruntule:odunc", nesne);
  console.log(`  ${ozne} -> ${nesne} : ${(k ? "izin" : "ret").padEnd(4)} ` +
    `(${String(kenarOkuma).padStart(2)} kenar okumasi)`);
}

console.log("\nerisilebilir nesnelerin listesi (tum desenler yurunur):");
for (const [ozne, izin] of [["P-2001", "goruntule:odunc"], ["U-1001", "goruntule:odunc"],
                            ["U-1006", "goruntule:liste"], ["U-1005", "goruntule:liste"]]) {
  kenarOkuma = 0;
  const liste = erisilebilirler(ozne, izin);
  console.log(`  ${ozne} ${izin.padEnd(16)} -> [${(liste.join(", ") || "bos").padEnd(22)}]` +
    ` (${kenarOkuma} kenar okumasi)`);
}
EOF
node rebac-maliyet.mjs
```

```text
tek nesne denetimi (ilk eslesen desende durur):
  P-2001 -> odunc:502 : izin (39 kenar okumasi)
  U-1001 -> odunc:501 : izin (13 kenar okumasi)
  U-1005 -> odunc:502 : ret  (65 kenar okumasi)

erisilebilir nesnelerin listesi (tum desenler yurunur):
  P-2001 goruntule:odunc  -> [odunc:501, odunc:502  ] (65 kenar okumasi)
  U-1001 goruntule:odunc  -> [odunc:501             ] (65 kenar okumasi)
  U-1006 goruntule:liste  -> [liste:9               ] (52 kenar okumasi)
  U-1005 goruntule:liste  -> [bos                   ] (52 kenar okumasi)
```

Kenar okuma sayıları bu küçük çizgeye özgüdür; oranların anlamı değişmez. İki gözlem
belirleyicidir.

Birincisi, **ret kararı en pahalı karardır**. İzin kararı ilk eşleşen desende durur; ret
kararı verilebilmesi için tüm desenlerin tüketilmiş olması gerekir. U-1005'in reddi altmış
beş kenar okumasına mal oldu, U-1001'in izni on üçe. Yetkisiz istek trafiğinin denetim
katmanına yüklediği maliyet, yetkili trafikten yüksektir; hız sınırı bu yüzden denetim
katmanının önünde durur.

İkincisi, listeleme sorgusu denetim sorgusunun tekrarı değildir. Bir liste sayfasını
"tüm kayıtları çek, her biri için denetim çağır" biçiminde çizmek, sayfa boyutu kadar
denetim çağrısı üretir. Doğru yapı, erişilebilir nesne kümesini bir kez üretip veri
sorgusunu o kümeyle sınırlamaktır. Bu kümenin sorguya nasıl geçirileceği ise ölçeğe bağlı
bir tasarım kararıdır: küçük kümeler doğrudan bir kimlik listesi olarak, büyük kümeler
kaynak sorgusuna eklenen bir birleştirme koşulu olarak taşınır.

#### Çizgenin Bakımı

İlişki tabanlı modelin işletimdeki maliyeti kenarların bakımıdır.

**Kenar, kaynak veriyle birlikte yazılır.** Bir ödünç kaydı oluşturulduğunda `sahip` kenarı
da oluşur; ikisi ayrı işlemlerde yazılırsa arada kalan istekler yanlış karara bağlanır.
Kenar yazımı, kaydın yazıldığı işlemin içinde olmalıdır.

**Kenar, kaynak veriyle birlikte silinir.** Silinmemiş kenarlar sessizce erişim açık
bırakır ve hiçbir hata üretmedikleri için fark edilmezler. Çizgede kaynağı olmayan
kenarların düzenli olarak taranması, İlişkisel Veritabanı Yönetimi kursundaki yetim kayıt
denetiminin buradaki karşılığıdır.

**Yol uzunluğu sınırlanır.** Desen ne kadar uzunsa arama o kadar geniş bir düğüm kümesine
yayılır. Sınırsız derinlikli bir devralma zinciri — grubun grubu, onun da grubu — hem
maliyeti öngörülemez kılar hem de kimin neye eriştiğini insan gözüyle izlenemez hâle
getirir.

#### Özet

- İlişki tabanlı erişim denetimi bağları birinci sınıf veri yapar; erişim sorusu çizge
  üzerinde bir ulaşılabilirlik sorgusuna dönüşür.
- Erişim kuralı bir yol desenidir; kenarlar ileri ya da ters yönde izlenebilir.
- Geçişken erişim — grupla paylaşılan bir kaydın grup üyelerine açılması — tek bir kenarla
  ifade edilir; öznitelik alanına kopyalama gerektirmez.
- Model bağlam kısıtlarını ifade edemez; saat gibi bilgilerin çizgede karşılığı yoktur.
- Ret kararı tüm desenlerin tüketilmesini gerektirdiği için izin kararından pahalıdır;
  listeleme sorgusu tek tek denetim çağrılarıyla değil, erişilebilir küme üretilerek yapılır.

#### Sonraki Adım

Üç modelin üçü de aynı senaryo kümesinde farklı yerlerden eksik kaldı ve eksikleri
birbirini tamamlıyor. Bu, dördüncü bir modele değil, kuralların yazıldığı yere ilişkin bir
soruya götürüyor: bu kuralların hepsi uygulama kodunun içinde dağınık biçimde mi duracak?
Sonraki ders kararı koddan ayıran politika tabanlı yetkilendirmeyi ele alıyor ve dört
modelin kararlarını aynı tabloda karşılaştırıyor.

### Politika Tabanlı Yetkilendirme

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/politika-tabanli-yetkilendirme

Erişim kararının uygulama kodundan ayrılıp yorumlanan bir politika belgesine taşınması, karar noktası ile uygulama noktası ayrımı, dört modelin aynı senaryo kümesindeki kararlarının karşılaştırılması ve politikanın rapor kipinde sınanması.

Üç ders üç model kurdu ve üçü de aynı sekiz senaryoda farklı yerlerden eksik kaldı. Rol
tabanlı model kaydın kimin olduğunu göremedi; öznitelik tabanlı model paylaşım bağını
göremedi; ilişki tabanlı model saati göremedi. Eksikler örtüşmüyor, birbirini tamamlıyor.

Bu gözlem dördüncü bir modele değil, başka bir soruya götürür: bu kurallar nerede yazılı
duracak? Şimdiye kadarki üç değerlendirici de birer betikti; gerçek bir uygulamada aynı
kurallar uç nokta işleyicilerinin içine, `if` bloklarına dağılmış olurdu. **Politika tabanlı
yetkilendirme (policy-based authorization)** kuralları koddan ayırıp yorumlanan bir belgeye
taşır.

#### Karar Noktası ve Uygulama Noktası

Ayrımın iki tarafı vardır. **Karar noktası (policy decision point)**, isteği ve ilgili
verileri alıp izin ya da ret üreten bileşendir; alan mantığı taşımaz, yalnız politikayı
yorumlar. **Uygulama noktası (policy enforcement point)**, kararı uygulayan yerdir: uç
nokta işleyicisi ya da ara katman. Kararı vermez, sorar ve sonucuna uyar.

Bu ayrımın üç somut kazancı vardır. Kural değişikliği kod değişikliği olmaktan çıkar;
politika belgesi ayrı sürümlenebilir ve dağıtımı koddan bağımsız yapılabilir. Kararın
gerekçesi tek bir yerde üretilir, dolayısıyla kütüğe yazılabilir ve sınanabilir. Aynı
politika birden çok servis tarafından kullanılabilir; kural kopyalanmaz.

Aşağıdaki motor bu ayrımı kuruyor. `POLITIKA` bir veri yapısıdır ve ayrı bir dosyada
tutulabilir; motor onu yorumlar ve kütüphaneye özgü hiçbir bilgi taşımaz.

```bash
cat > politika.mjs <<'EOF'
const KENARLAR = [
  ["U-1001", "sahip", "odunc:501"], ["U-1002", "sahip", "odunc:502"],
  ["U-1005", "sahip", "odunc:503"], ["U-1001", "sahip", "liste:9"],
  ["odunc:501", "ait", "S-02"], ["odunc:502", "ait", "S-02"],
  ["odunc:503", "ait", "S-01"], ["ceza:77", "ait", "S-02"],
  ["P-2001", "calisir", "S-02"], ["Y-3001", "yonetir", "S-02"],
  ["liste:9", "paylasildi", "U-1002"], ["liste:9", "paylasildi", "grup:okuma-kulubu"],
  ["U-1006", "uyesi", "grup:okuma-kulubu"],
];
const OZNE = {
  "U-1001": { rol: "uye" }, "U-1002": { rol: "uye" }, "U-1005": { rol: "uye" },
  "P-2001": { rol: "gorevli" }, "Y-3001": { rol: "sube_yoneticisi" },
};

// --- Politika belgesi: veri, kod degil. Ayri tutulur ve surumlenir. ---
const POLITIKA = {
  surum: "2026-03-01",
  kurallar: [
    { ad: "kendi kaydini gorur", etki: "izin", eylem: ["goruntule"],
      nesne: ["odunc", "liste"], iliski: [["sahip"]] },
    { ad: "sube gorevlisi", etki: "izin", eylem: ["goruntule", "guncelle", "olustur"],
      nesne: ["odunc"], rol: ["gorevli", "sube_yoneticisi"],
      iliski: [["calisir", "~ait"], ["yonetir", "~ait"]] },
    { ad: "yonetici ceza siler", etki: "izin", eylem: ["sil"], nesne: ["ceza"],
      rol: ["sube_yoneticisi"], iliski: [["yonetir", "~ait"]] },
    { ad: "paylasilan liste", etki: "izin", eylem: ["goruntule"], nesne: ["liste"],
      iliski: [["~paylasildi"], ["uyesi", "~paylasildi"]] },
    { ad: "mesai disi yazma yasak", etki: "ret", eylem: ["guncelle", "olustur", "sil"],
      nesne: ["odunc", "ceza", "liste"], saat_disinda: ["08:30", "18:00"],
      rol_haric: ["sube_yoneticisi"] },
  ],
};

// --- Motor: politikayi yorumlar, alan bilgisi tasimaz. ---
function adim(dugumler, iliski) {
  const ters = iliski.startsWith("~"), ad = ters ? iliski.slice(1) : iliski;
  const sonuc = new Set();
  for (const [k, i, h] of KENARLAR) {
    if (i !== ad) continue;
    if (!ters && dugumler.has(k)) sonuc.add(h);
    if (ters && dugumler.has(h)) sonuc.add(k);
  }
  return sonuc;
}

const ulasir = (ozne, desenler, nesne) => desenler.some((desen) => {
  let cephe = new Set([ozne]);
  for (const i of desen) cephe = adim(cephe, i);
  return cephe.has(nesne);
});

function uyar(kural, { ozne, eylem, nesne, saat }) {
  const rol = OZNE[ozne]?.rol;
  if (!kural.eylem.includes(eylem)) return false;
  if (!kural.nesne.includes(nesne.split(":")[0])) return false;
  if (kural.rol && !kural.rol.includes(rol)) return false;
  if (kural.rol_haric && kural.rol_haric.includes(rol)) return false;
  if (kural.saat_disinda) {
    const [bas, bit] = kural.saat_disinda;
    if (saat >= bas && saat <= bit) return false;
  }
  if (kural.iliski && !ulasir(ozne, kural.iliski, nesne)) return false;
  return true;
}

function karar(istek) {
  const eslesen = POLITIKA.kurallar.filter((k) => uyar(k, istek));
  const ret = eslesen.find((k) => k.etki === "ret");
  if (ret) return [false, ret.ad + " (ret oncelikli)"];
  const izin = eslesen.find((k) => k.etki === "izin");
  return izin ? [true, izin.ad] : [false, "eslesen kural yok (varsayilan ret)"];
}

const SENARYOLAR = [
  ["S1", "U-1001", "goruntule", "odunc:501", "10:15", true],
  ["S2", "U-1001", "goruntule", "odunc:502", "10:16", false],
  ["S3", "P-2001", "goruntule", "odunc:502", "10:20", true],
  ["S4", "P-2001", "goruntule", "odunc:503", "10:21", false],
  ["S5", "P-2001", "guncelle",  "odunc:502", "22:40", false],
  ["S6", "Y-3001", "sil",       "ceza:77",   "11:05", true],
  ["S7", "U-1002", "goruntule", "liste:9",   "12:00", true],
  ["S8", "U-1005", "goruntule", "liste:9",   "12:01", false],
];

console.log(`politika surumu: ${POLITIKA.surum}, kural sayisi: ${POLITIKA.kurallar.length}`);
console.log("\nsenaryo | politika | dogru | uyum | karari veren kural");
console.log("--------|----------|-------|------|--------------------------------");
let uyan = 0;
for (const [ad, ozne, eylem, nesne, saat, dogru] of SENARYOLAR) {
  const [k, gerekce] = karar({ ozne, eylem, nesne, saat });
  if (k === dogru) uyan++;
  console.log(`${ad}      | ${(k ? "izin" : "ret").padEnd(8)} | ` +
    `${(dogru ? "izin" : "ret").padEnd(5)} | ${(k === dogru ? "+" : "-").padEnd(4)} | ${gerekce}`);
}
console.log(`\npolitika motoru dogru karar: ${uyan}/${SENARYOLAR.length}`);
EOF
node politika.mjs
```

```text
politika surumu: 2026-03-01, kural sayisi: 5

senaryo | politika | dogru | uyum | karari veren kural
--------|----------|-------|------|--------------------------------
S1      | izin     | izin  | +    | kendi kaydini gorur
S2      | ret      | ret   | +    | eslesen kural yok (varsayilan ret)
S3      | izin     | izin  | +    | sube gorevlisi
S4      | ret      | ret   | +    | eslesen kural yok (varsayilan ret)
S5      | ret      | ret   | +    | mesai disi yazma yasak (ret oncelikli)
S6      | izin     | izin  | +    | yonetici ceza siler
S7      | izin     | izin  | +    | paylasilan liste
S8      | ret      | ret   | +    | eslesen kural yok (varsayilan ret)

politika motoru dogru karar: 8/8
```

Sekiz karar da doğru. Kazanç yeni bir modelden değil, üç modelin ifade gücünün tek bir
belgede birleşmesinden geliyor: `rol` alanı rol tabanlı, `saat_disinda` öznitelik tabanlı,
`iliski` ilişki tabanlı kısıtı taşıyor. Motor bunları ayrı kavramlar olarak değil, aynı
kuralın alanları olarak görüyor.

İki tasarım kararı belgede yazılıdır. **Varsayılan ret**: hiçbir kural eşleşmezse karar
rettir. **Ret önceliği**: eşleşen kurallardan biri ret diyorsa izin diyenler sayılmaz.
İkisi birlikte, politikaya yeni bir kural eklemenin var olan bir yasağı sessizce
gevşetememesini sağlar.

#### Dört Modelin Karşılaştırması

Aşağıdaki blok dört modeli de aynı kümede çalıştırıp kararları yan yana koyuyor.

```bash
cat > karsilastirma.mjs <<'EOF'
const OZNE = {
  "U-1001": { rol: "uye", sube: "S-02" }, "U-1002": { rol: "uye", sube: "S-02" },
  "U-1005": { rol: "uye", sube: "S-01" }, "P-2001": { rol: "gorevli", sube: "S-02" },
  "Y-3001": { rol: "sube_yoneticisi", sube: "S-02" },
};
const KENARLAR = [
  ["U-1001", "sahip", "odunc:501"], ["U-1002", "sahip", "odunc:502"],
  ["U-1005", "sahip", "odunc:503"], ["U-1001", "sahip", "liste:9"],
  ["odunc:501", "ait", "S-02"], ["odunc:502", "ait", "S-02"],
  ["odunc:503", "ait", "S-01"], ["ceza:77", "ait", "S-02"],
  ["P-2001", "calisir", "S-02"], ["Y-3001", "yonetir", "S-02"],
  ["liste:9", "paylasildi", "U-1002"],
];
const SENARYOLAR = [
  ["S1", "U-1001", "goruntule", "odunc:501", { sahip: "U-1001", sube: "S-02" }, "10:15", true],
  ["S2", "U-1001", "goruntule", "odunc:502", { sahip: "U-1002", sube: "S-02" }, "10:16", false],
  ["S3", "P-2001", "goruntule", "odunc:502", { sahip: "U-1002", sube: "S-02" }, "10:20", true],
  ["S4", "P-2001", "goruntule", "odunc:503", { sahip: "U-1005", sube: "S-01" }, "10:21", false],
  ["S5", "P-2001", "guncelle",  "odunc:502", { sahip: "U-1002", sube: "S-02" }, "22:40", false],
  ["S6", "Y-3001", "sil",       "ceza:77",   { sahip: "U-1002", sube: "S-02" }, "11:05", true],
  ["S7", "U-1002", "goruntule", "liste:9",   { sahip: "U-1001", sube: "S-02" }, "12:00", true],
  ["S8", "U-1005", "goruntule", "liste:9",   { sahip: "U-1001", sube: "S-02" }, "12:01", false],
];
const tur = (n) => n.split(":")[0];
const mesai = (s) => s >= "08:30" && s <= "18:00";
const YAZAN = ["guncelle", "olustur", "sil"];

// 1. Rol tabanli: karar yalniz role ve kaynak turune bakar.
const IZIN = {
  uye: ["goruntule:odunc", "goruntule:liste"],
  gorevli: ["goruntule:odunc", "goruntule:liste", "guncelle:odunc", "olustur:odunc"],
  sube_yoneticisi: ["goruntule:odunc", "goruntule:liste", "guncelle:odunc",
                    "olustur:odunc", "sil:ceza", "sil:odunc"],
};
const rbac = (o, e, n) => IZIN[OZNE[o].rol].includes(`${e}:${tur(n)}`);

// 2. Oznitelik tabanli: sahiplik, sube ve saat karsilastirmalari.
function abac(o, e, n, oz, saat) {
  const s = OZNE[o];
  if (YAZAN.includes(e) && !mesai(saat) && s.rol !== "sube_yoneticisi") return false;
  if (e === "goruntule" && oz.sahip === o) return true;
  if (["gorevli", "sube_yoneticisi"].includes(s.rol) && oz.sube === s.sube &&
      ["goruntule", "guncelle", "olustur"].includes(e)) return true;
  return s.rol === "sube_yoneticisi" && oz.sube === s.sube && e === "sil" && tur(n) === "ceza";
}

// 3. Iliski tabanli: cizge uzerinde yol deseni.
const DESEN = {
  "goruntule:odunc": [["sahip"], ["calisir", "~ait"], ["yonetir", "~ait"]],
  "guncelle:odunc": [["calisir", "~ait"], ["yonetir", "~ait"]],
  "sil:ceza": [["yonetir", "~ait"]],
  "goruntule:liste": [["sahip"], ["~paylasildi"]],
};
function adim(kume, il) {
  const ters = il.startsWith("~"), ad = ters ? il.slice(1) : il, c = new Set();
  for (const [k, i, h] of KENARLAR) {
    if (i !== ad) continue;
    if (!ters && kume.has(k)) c.add(h);
    if (ters && kume.has(h)) c.add(k);
  }
  return c;
}
const ulasir = (o, desenler, n) => (desenler ?? []).some((d) => {
  let c = new Set([o]);
  for (const i of d) c = adim(c, i);
  return c.has(n);
});
const rebac = (o, e, n) => ulasir(o, DESEN[`${e}:${tur(n)}`], n);

// 4. Politika tabanli: rol, iliski ve baglam tek belgede.
const POLITIKA = [
  { etki: "ret", eylem: YAZAN, saat_disinda: ["08:30", "18:00"], rol_haric: ["sube_yoneticisi"] },
  { etki: "izin", eylem: ["goruntule"], desen: [["sahip"], ["~paylasildi"]] },
  { etki: "izin", eylem: ["goruntule", "guncelle", "olustur"],
    rol: ["gorevli", "sube_yoneticisi"], desen: [["calisir", "~ait"], ["yonetir", "~ait"]] },
  { etki: "izin", eylem: ["sil"], rol: ["sube_yoneticisi"], desen: [["yonetir", "~ait"]] },
];
function politika(o, e, n, oz, saat) {
  const rol = OZNE[o].rol;
  const uyan = POLITIKA.filter((k) =>
    k.eylem.includes(e) &&
    !(k.rol && !k.rol.includes(rol)) &&
    !(k.rol_haric && k.rol_haric.includes(rol)) &&
    !(k.saat_disinda && saat >= k.saat_disinda[0] && saat <= k.saat_disinda[1]) &&
    !(k.desen && !ulasir(o, k.desen, n)));
  if (uyan.some((k) => k.etki === "ret")) return false;
  return uyan.some((k) => k.etki === "izin");
}

const MODELLER = [["RBAC", rbac], ["ABAC", abac], ["ReBAC", rebac], ["politika", politika]];
const sayac = new Map(MODELLER.map(([ad]) => [ad, 0]));
const ayirac = "--------|" + MODELLER.map(() => "---------").join("|") + "|-------";

console.log("senaryo | " + MODELLER.map(([a]) => a.padEnd(8)).join("| ") + "| dogru");
console.log(ayirac);
for (const [ad, o, e, n, oz, saat, dogru] of SENARYOLAR) {
  const hucreler = MODELLER.map(([m, fn]) => {
    const k = fn(o, e, n, oz, saat);
    if (k === dogru) sayac.set(m, sayac.get(m) + 1);
    return `${k ? "izin" : "ret "} ${k === dogru ? "+" : "-"}`.padEnd(8);
  });
  console.log(`${ad}      | ${hucreler.join("| ")}| ${dogru ? "izin" : "ret"}`);
}
console.log(ayirac);
console.log("dogru   | " + MODELLER.map(([m]) => `${sayac.get(m)}/8`.padEnd(8)).join("| ") + "|");
EOF
node karsilastirma.mjs
```

```text
senaryo | RBAC    | ABAC    | ReBAC   | politika| dogru
--------|---------|---------|---------|---------|-------
S1      | izin +  | izin +  | izin +  | izin +  | izin
S2      | izin -  | ret  +  | ret  +  | ret  +  | ret
S3      | izin +  | izin +  | izin +  | izin +  | izin
S4      | izin -  | ret  +  | ret  +  | ret  +  | ret
S5      | izin -  | ret  +  | izin -  | ret  +  | ret
S6      | izin +  | izin +  | izin +  | izin +  | izin
S7      | izin +  | ret  -  | izin +  | izin +  | izin
S8      | izin -  | ret  +  | ret  +  | ret  +  | ret
--------|---------|---------|---------|---------|-------
dogru   | 4/8     | 7/8     | 7/8     | 8/8     |
```

Tablonun okunma biçimi önemlidir. Sağdaki sütunun soldakinden "daha iyi" olduğu sonucu
çıkarılmamalıdır; çıkarılacak sonuç, her modelin ifade edebildiği kısıt türünün farklı
olduğudur. Rol tabanlı modelin dört doğrusu, kararın kaynak türünde bittiği durumlarda
başka hiçbir düzeneğe gerek olmadığını gösterir.

S5 ve S7 sütunları modellerin sınırlarını aynı satırda okutuyor: öznitelik tabanlı model
S5'i çözüyor S7'yi çözemiyor, ilişki tabanlı model tersini yapıyor. Politika sütunu ikisini
birden çözüyor, çünkü kural belgesi iki kısıt türünü de alan olarak taşıyor.

#### Politikanın İşletimi

Politikanın ayrı bir belge olması, kod dışı bir dağıtım nesnesi ürettiği anlamına gelir ve
bunun kendi disiplini vardır.

**Politika sürümlenir ve kararla birlikte kaydedilir.** Kütüğe yazılan satır, kararın
kendisini ve onu veren kuralın adını içerir. "Bu istek neden reddedildi" sorusunun yanıtı,
aylar sonra da verilebilir olur.

**Değişiklik rapor kipinde sınanır.** İstemci Güvenliği konusundaki içerik güvenlik
politikası için kurulan ayrım burada da geçerlidir: yeni politika, gerçek trafikte karar
üretir ama kararı uygulanmaz; yalnız yürürlükteki politikayla farkı kaydedilir. Fark
listesi beklenen değişikliklerden ibaretse zorlama kipine geçilir.

**Senaryo kümesi politikanın testidir.** Bu dersteki tablo bir sınama düzeneğidir; her
politika değişikliğinde çalıştırılır ve uyum sütununda eksi çıkması değişikliği durdurur.
Kümeye, her yeni kuralla birlikte o kuralın izin verdiği ve reddettiği birer senaryo
eklenir.

**Karar verisi karar noktasına taşınır.** Motorun ilişki çizgesine ve öznitelik değerlerine
erişmesi gerekir. Bu veri karar anında sorgulanıyorsa her istek bir ek gecikme taşır;
önceden kopyalanıyorsa tazelik sorunu doğar. Ölçüt, öznitelik tazeliği dersindeki ölçütün
aynısıdır: kararı değiştiren ve değişebilen veri karar anında okunur.

#### Özet

- Politika tabanlı yetkilendirme erişim kurallarını uygulama kodundan ayırıp yorumlanan bir
  belgeye taşır; karar noktası politikayı yorumlar, uygulama noktası kararı uygular.
- Belge rol, öznitelik ve ilişki kısıtlarını aynı kuralın alanları olarak taşıyabildiği
  için üç modelin ifade gücü birleşir.
- Varsayılan ret ve ret önceliği birlikte, yeni bir kuralın var olan bir yasağı sessizce
  gevşetmesini engeller.
- Dört model aynı senaryo kümesinde sırasıyla 4, 7, 7 ve 8 doğru karar verir; fark modelin
  kalitesi değil, ifade edebildiği kısıt türüdür.
- Politika sürümlenir, kararın gerekçesiyle birlikte kütüklenir ve değişiklikler rapor
  kipinde sınandıktan sonra zorlama kipine alınır.

#### Sonraki Adım

Buraya kadarki dört model, kararı isteği yapan kişinin kimliği üzerinden verdi. Oysa
isteği yapan çoğu zaman kişinin kendisi değil, kişinin yetkilendirdiği bir istemcidir:
kütüphanenin mobil uygulaması, bir rapor aracı, üçüncü taraf bir okuma listesi servisi.
Kişinin yetkisi ile istemciye verilen yetki aynı şey değildir. Sonraki ders bu ikinci
katmanı — belirteç kapsamlarını — ele alıyor ve kapsamların uç noktalara nasıl eşlendiğini
gösteriyor.

### Kapsamlar ve İzinler

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/kapsamlar-ve-izinler

Belirteç kapsamının sahibinin yetkisinden dar tutulması, kapsam ve izin katmanlarının kesişimiyle verilen karar, reddin nedenine göre ayrışan yanıtlar, erişim yüzeyinin ölçülmesi ve yenilemede daraltma kuralı.

Önceki dört ders kararı **kişiye** bağladı: kim hangi rolde, hangi özniteliklere sahip,
nesneyle nasıl bir ilişkisi var. Bu soruların yanıtı bir kişi için sabittir. Ancak isteği
yapan çoğu zaman kişinin kendisi değildir: kütüphanenin mobil uygulaması, bir rapor aracı,
kişinin yetkilendirdiği üçüncü taraf bir okuma listesi servisi.

Bu istemcilere kişinin bütün yetkisini vermek gereksizdir. Okuma listesi servisinin ödünç
kaydı silmeye ihtiyacı yoktur; rapor aracının üye adresini okumaya ihtiyacı yoktur.
**Kapsam (scope)**, bir belirtecin taşıdığı yetkinin, sahibinin yetkisinden **daha dar**
olmasını sağlayan mekanizmadır.

#### İki Katmanlı Karar

Kapsam, önceki derslerdeki yetki modellerinin yerine geçmez; üstüne biner. Bir istek iki
soruyu birden geçmek zorundadır: **kişi bunu yapabilir mi** ve **bu istemciye bu yetki
verilmiş mi**. İkisi ayrı kaynaklardan gelir — birincisi kullanıcı yönetiminden, ikincisi
belirtecin verildiği andaki onaydan.

Karar bu yüzden bir kesişimdir. Kapsam yetkiyi genişletemez: bir belirteçte `ceza:yaz`
kapsamı bulunması, sahibinin ceza tahsil etme izni yoksa hiçbir şey ifade etmez. Tersi de
geçerlidir: yetkili bir görevlinin belirteci, kapsamı dışındaki uç noktaya erişemez.

Aşağıdaki hesap bu iki katmanı bir eşleme tablosu üzerinde çalıştırır.

```bash
cat > kapsam.mjs <<'EOF'
// kapsam.mjs — belirtec kapsamlarinin uc noktalara eslenmesi ve iki katmanli karar

// 1. Uc nokta -> gereken kapsam eslemesi. Sozlesmenin parcasidir.
const UC_NOKTALAR = [
  { yontem: "GET",    yol: "/oduncler",            kapsam: "odunc:oku" },
  { yontem: "POST",   yol: "/oduncler",            kapsam: "odunc:yaz" },
  { yontem: "DELETE", yol: "/oduncler/{id}",       kapsam: "odunc:yaz" },
  { yontem: "GET",    yol: "/uyeler/{id}",         kapsam: "uye:oku" },
  { yontem: "PATCH",  yol: "/uyeler/{id}",         kapsam: "uye:yaz" },
  { yontem: "GET",    yol: "/uyeler/{id}/cezalar", kapsam: "ceza:oku" },
  { yontem: "POST",   yol: "/cezalar/{id}/tahsil", kapsam: "ceza:yaz" },
  { yontem: "GET",    yol: "/raporlar/gecikme",    kapsam: "rapor:oku" },
];

// 2. Ozne izinleri (RBAC dersinden): kisinin yapabildikleri.
const IZINLER = {
  "U-1001": new Set(["odunc:oku", "odunc:yaz", "uye:oku", "ceza:oku"]),
  "P-2001": new Set(["odunc:oku", "odunc:yaz", "uye:oku", "uye:yaz", "ceza:oku", "ceza:yaz"]),
  "Y-3001": new Set(["odunc:oku", "odunc:yaz", "uye:oku", "uye:yaz", "ceza:oku", "ceza:yaz", "rapor:oku"]),
};

// 3. Belirtecler: istemciye verilen kapsam kumesi. Kisinin izinleriyle ayni sey degildir.
const BELIRTECLER = [
  { ad: "mobil-uygulama", ozne: "U-1001", kapsamlar: ["odunc:oku", "odunc:yaz", "uye:oku"] },
  { ad: "okuma-listesi",  ozne: "U-1001", kapsamlar: ["odunc:oku"] },
  { ad: "rapor-araci",    ozne: "P-2001", kapsamlar: ["rapor:oku", "odunc:oku"] },
  { ad: "banko-uygulamasi", ozne: "Y-3001", kapsamlar: ["odunc:oku", "odunc:yaz", "uye:yaz", "ceza:yaz"] },
];

// 4. Karar: iki katman da gecmeli. Kapsam istemcinin yetkisini, izin kisinin yetkisini sinirlar.
function karar(belirtec, uc) {
  const kapsamVar = belirtec.kapsamlar.includes(uc.kapsam);
  const izinVar = IZINLER[belirtec.ozne].has(uc.kapsam);
  if (!kapsamVar && !izinVar) return "red: kapsam ve izin yok";
  if (!kapsamVar) return "red: belirtecin kapsami yok";
  if (!izinVar) return "red: oznenin izni yok";
  return "izin";
}

const dolgu = (s, n) => String(s).padEnd(n);
console.log("belirtec           uc nokta                          sonuc");
for (const b of BELIRTECLER) {
  for (const uc of UC_NOKTALAR) {
    const s = karar(b, uc);
    if (s === "izin") continue;                       // yalniz reddedilenleri goster
    if (!IZINLER[b.ozne].has(uc.kapsam) && !b.kapsamlar.includes(uc.kapsam)) continue;
    console.log(`${dolgu(b.ad, 18)} ${dolgu(uc.yontem + " " + uc.yol, 33)} ${s}`);
  }
}

// 5. Kapsamin erisim yuzeyi: her belirtec kac uc noktaya ulasabiliyor?
console.log("\nbelirtec           kapsam  izinli uc nokta  yuzey");
for (const b of BELIRTECLER) {
  const izinli = UC_NOKTALAR.filter((uc) => karar(b, uc) === "izin");
  const yuzde = ((100 * izinli.length) / UC_NOKTALAR.length).toFixed(0);
  console.log(`${dolgu(b.ad, 18)} ${dolgu(b.kapsamlar.length, 7)} ${dolgu(izinli.length + " / " + UC_NOKTALAR.length, 16)} %${yuzde}`);
}

// 6. Kapsam taneciginin etkisi: kaba kapsam ile ince kapsamin yuzeyi.
const KABA = ["odunc:*", "uye:*"];
const kabaEsler = (uc) => KABA.some((k) => uc.kapsam.startsWith(k.slice(0, -1)));
const kabaYuzey = UC_NOKTALAR.filter(kabaEsler).length;
const inceYuzey = UC_NOKTALAR.filter((uc) => ["odunc:oku", "uye:oku"].includes(uc.kapsam)).length;
console.log(`\nkaba kapsam ${JSON.stringify(KABA)} -> ${kabaYuzey} / ${UC_NOKTALAR.length} uc nokta`);
console.log(`ince kapsam ["odunc:oku","uye:oku"] -> ${inceYuzey} / ${UC_NOKTALAR.length} uc nokta`);
console.log(`okuma isteyen bir istemciye kaba kapsam verilirse fazladan ${kabaYuzey - inceYuzey} uc nokta acilir.`);

// 7. Yenilemede daraltma: istemci daha az kapsam isteyebilir, daha cogunu isteyemez.
function yenile(belirtec, istenen) {
  const disarida = istenen.filter((k) => !belirtec.kapsamlar.includes(k));
  return disarida.length
    ? { sonuc: "red", neden: "istenen kapsam ilk belirtecin disinda: " + disarida.join(", ") }
    : { sonuc: "kabul", kapsamlar: istenen };
}
const mobil = BELIRTECLER[0];
console.log("\nyenileme denemeleri (ilk kapsam: " + JSON.stringify(mobil.kapsamlar) + ")");
for (const istenen of [["odunc:oku"], ["odunc:oku", "uye:oku"], ["odunc:oku", "ceza:yaz"]]) {
  const y = yenile(mobil, istenen);
  console.log(`  ${dolgu(JSON.stringify(istenen), 34)} ${y.sonuc}${y.neden ? " — " + y.neden : ""}`);
}
EOF
node kapsam.mjs
```

```text
belirtec           uc nokta                          sonuc
mobil-uygulama     GET /uyeler/{id}/cezalar          red: belirtecin kapsami yok
okuma-listesi      POST /oduncler                    red: belirtecin kapsami yok
okuma-listesi      DELETE /oduncler/{id}             red: belirtecin kapsami yok
okuma-listesi      GET /uyeler/{id}                  red: belirtecin kapsami yok
okuma-listesi      GET /uyeler/{id}/cezalar          red: belirtecin kapsami yok
rapor-araci        POST /oduncler                    red: belirtecin kapsami yok
rapor-araci        DELETE /oduncler/{id}             red: belirtecin kapsami yok
rapor-araci        GET /uyeler/{id}                  red: belirtecin kapsami yok
rapor-araci        PATCH /uyeler/{id}                red: belirtecin kapsami yok
rapor-araci        GET /uyeler/{id}/cezalar          red: belirtecin kapsami yok
rapor-araci        POST /cezalar/{id}/tahsil         red: belirtecin kapsami yok
rapor-araci        GET /raporlar/gecikme             red: oznenin izni yok
banko-uygulamasi   GET /uyeler/{id}                  red: belirtecin kapsami yok
banko-uygulamasi   GET /uyeler/{id}/cezalar          red: belirtecin kapsami yok
banko-uygulamasi   GET /raporlar/gecikme             red: belirtecin kapsami yok

belirtec           kapsam  izinli uc nokta  yuzey
mobil-uygulama     3       4 / 8            %50
okuma-listesi      1       1 / 8            %13
rapor-araci        2       1 / 8            %13
banko-uygulamasi   4       5 / 8            %63

kaba kapsam ["odunc:*","uye:*"] -> 5 / 8 uc nokta
ince kapsam ["odunc:oku","uye:oku"] -> 2 / 8 uc nokta
okuma isteyen bir istemciye kaba kapsam verilirse fazladan 3 uc nokta acilir.

yenileme denemeleri (ilk kapsam: ["odunc:oku","odunc:yaz","uye:oku"])
  ["odunc:oku"]                      kabul
  ["odunc:oku","uye:oku"]            kabul
  ["odunc:oku","ceza:yaz"]           red — istenen kapsam ilk belirtecin disinda: ceza:yaz
```

#### Reddin Nedeni Ayırt Edilebilir Olmalı

Çıktının ilk bölümü reddedilen istekleri nedenleriyle listeliyor. İki neden birbirinden
ayrıdır ve bu ayrım geliştiriciye farklı şey söyler.

"Belirtecin kapsamı yok" onarılabilir bir durumdur: istemci kullanıcıdan ek onay isteyip
daha geniş kapsamlı bir belirteç alabilir. "Öznenin izni yok" ise onarılamaz; hiçbir onay
akışı görevliye rapor okuma izni veremez, çünkü o izin kullanıcı yönetiminde tanımlıdır.
Rapor aracı satırı bunun örneğidir: belirteçte `rapor:oku` kapsamı vardır ama öznenin izni
yoktur, dolayısıyla istek reddedilir.

İstemciye dönen yanıt bu ayrımı taşımalıdır. Yetersiz kapsam için standart, hangi kapsamın
gerektiğini bildiren bir yanıttır; yetersiz izin için ise ek bilgi verilmez — kullanıcının
neye yetkili olmadığını ayrıntılandırmak, yetki haritasını dışarıya sızdırır.

#### Kapsam Bir Erişim Yüzeyidir

İkinci bölüm her belirtecin kaç uç noktaya ulaşabildiğini sayıyor. Bu sayı, belirteç
sızdığında ne kadar zarar verilebileceğinin doğrudan ölçüsüdür: okuma listesi servisinin
belirteci sekiz uç noktanın birine, banko uygulamasının belirteci beşine erişir.

Yüzeyin ölçülebilir olması bir tasarım aracıdır. Bir istemcinin istediği kapsam kümesi
onaylanmadan önce sorulacak soru şudur: bu kümenin açtığı uç nokta sayısı, istemcinin
gerçekten yaptığı işle orantılı mı? Okuma listesi servisi yalnız ödünç geçmişini okuyorsa
`odunc:oku` yeterlidir; `odunc:yaz` istemesi, yüzeyi üç katına çıkarır ve gerekçesi
yoktur.

#### Tanecik Boyutu Bir Karardır

Üçüncü bölüm kaba ve ince kapsamı karşılaştırıyor. Kaynak başına tek bir kapsam
tanımlamak — `odunc:*` gibi — kapsam listesini kısaltır ama okuma isteyen bir istemciye
yazma yetkisini de verir; modelde bu, fazladan üç uç nokta demektir.

Ters uçta her uç nokta için ayrı kapsam tanımlamak da işe yaramaz: kapsam listesi
büyüdükçe onay ekranı okunamaz hâle gelir ve kullanıcı listeyi okumadan onaylar. İşe
yarayan tanecik, **kaynak ve eylem** ikilisidir: `odunc:oku`, `odunc:yaz`. Bu düzeyde
kapsam sayısı kaynak sayısının iki katıdır ve her kapsamın karşılığı bir cümleyle
anlatılabilir.

Kapsam adının kendisi de sözleşmenin parçasıdır. Uç nokta değiştiğinde kapsam adı sabit
kalır; kapsam adı değiştiğinde bütün istemcilerin yeniden onay alması gerekir. Bu, API
Sürümleme dersindeki kırıcı değişiklik kuralının yetkilendirme tarafındaki karşılığıdır.

#### Yenilemede Daraltma Vardır, Genişletme Yoktur

Son bölüm yenileme davranışını gösteriyor. Bir istemci elindeki belirteci yenilerken daha
**dar** bir kapsam kümesi isteyebilir; bu, uzun ömürlü bir oturumda geçici olarak daha az
yetkiyle çalışmayı mümkün kılar. Daha **geniş** bir küme isteyemez: genişletme, yeni bir
onay akışı gerektirir.

Kural tek cümleyle yazılır: yenilenen belirtecin kapsamı, ilk belirtecin kapsamının alt
kümesi olmalıdır. Bu kuralın uygulanmaması, yenileme uç noktasını sessiz bir yetki
yükseltme yoluna çevirir — çünkü yenileme, kullanıcı onayı olmadan çalışan bir akıştır.

#### Özet

- Kapsam, belirtecin yetkisini sahibinin yetkisinden daha dar tutar; karar iki katmanın
  kesişimidir ve kapsam hiçbir zaman yetkiyi genişletmez.
- Yetersiz kapsam ile yetersiz izin farklı yanıtlar üretir: birincisi hangi kapsamın
  gerektiğini bildirir, ikincisi ayrıntı vermez.
- Bir belirtecin eriştiği uç nokta sayısı, sızma durumundaki zararın ölçüsüdür; modelde
  belirteçlerin yüzeyi yüzde on üç ile yüzde altmış üç arasında değişiyor.
- Tanecik kaynak–eylem düzeyinde tutulur: kaba kapsam okuma isteyen istemciye üç fazladan
  uç nokta açar, aşırı ince kapsam onay ekranını okunamaz yapar.
- Yenilemede kapsam yalnız daraltılabilir; genişletme yeni onay gerektirir, aksi hâlde
  yenileme sessiz bir yetki yükseltme yoluna dönüşür.

#### Sonraki Adım

Buraya kadarki bütün kararlar tek bir kütüphanenin içinde verildi: bir üye, bir görevli,
bir şube. Aynı sunucu birden çok kütüphaneye hizmet verdiğinde araya yeni bir sınır girer
ve bu sınır rol ya da kapsamla değil **veriyle** çizilir: bir kütüphanenin görevlisi, kendi
kurumunun kayıtlarını görebilmeli, komşu kurumunkileri hiçbir koşulda görememelidir.
Sonraki ders bu yalıtımı ele alır ve sınırın sorgunun kendisine nasıl taşındığını gösterir.

### Çok Kiracılı Yetkilendirme

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/cok-kiracili-yetkilendirme

Kiracı sınırının sorguya taşınması, süzgeç unutulduğunda oluşan sızıntının ölçülmesi, kiracı kimliğinin kimliği doğrulanmış bağlamdan alınması, üç katmanlı savunma ve fiziksel yerleşim seçenekleri.

Önceki ders kapsamı bir istemcinin yetkisini daraltmak için kullandı. Bütün örneklerde
arka planda tek bir kütüphane vardı: bir üye kümesi, bir görevli kadrosu, bir kayıt havuzu.
Aynı sunucu birden çok kuruma hizmet verdiğinde araya yeni bir sınır girer.

Bu sınırın önceki modellerden farkı, **veriyle** çizilmesidir. Kuzey Kütüphanesi'nin
görevlisi kendi kurumunun bütün kayıtlarını görebilir; Güney Kütüphanesi'nin aynı roldeki
görevlisi de kendi kurumunun kayıtlarını görebilir. İkisinin rolü, izinleri ve belirteç
kapsamları aynıdır. Ayıran tek şey, sorgunun hangi satırlara dokunduğudur.

#### Yalıtım Rolle Değil Sorguyla Kurulur

Çok kiracılı bir sistemde her kayıt bir **kiracıya (tenant)** aittir. Kiracı, verinin
sahibi olan kurumdur ve şemada bir sütun olarak durur. Yetki denetimi bu sütuna
bakmadığında rol denetimi geçen bir istek, komşu kurumun kaydını döndürür.

Aşağıdaki hesap iki depo gerçekleştirimini aynı veri üzerinde çalıştırır: biri kiracı
süzgecini unutmuş, diğeri her sorguda zorunlu tutuyor.

```bash
cat > kiraci.mjs <<'EOF'
// kiraci.mjs — kiraci yalitiminin sorguya tasinmasi ve denetlenmesi
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync(":memory:");
db.exec(`
  CREATE TABLE uye (uye_id TEXT PRIMARY KEY, kiraci TEXT NOT NULL, ad TEXT NOT NULL);
  CREATE TABLE odunc (odunc_id TEXT PRIMARY KEY, kiraci TEXT NOT NULL, uye_id TEXT NOT NULL,
                      isbn TEXT NOT NULL, durum TEXT NOT NULL);
  INSERT INTO uye VALUES ('U-1','kuzey','Ayse Yilmaz'), ('U-2','kuzey','Deniz Kaya'),
                         ('U-9','guney','Selin Aydin');
  INSERT INTO odunc VALUES ('O-1','kuzey','U-1','978-0201896831','ACIK'),
                           ('O-2','kuzey','U-2','978-0262033848','KAPALI'),
                           ('O-7','guney','U-9','978-0131103627','ACIK'),
                           ('O-8','guney','U-9','978-0201896831','ACIK');
`);

// 1. Kiraci suzgeci unutulmus depo: yalnizca durum kosulu var.
const gevsek = {
  ad: "kiraci suzgeci yok",
  acikOduncler: () => db.prepare("SELECT odunc_id, kiraci FROM odunc WHERE durum = 'ACIK'").all(),
  uyeBul: (id) => db.prepare("SELECT uye_id, kiraci, ad FROM uye WHERE uye_id = ?").all(id),
};

// 2. Kiraci suzgeci zorunlu depo: her sorgu baglamdaki kiraciyi alir.
const siki = {
  ad: "kiraci suzgeci zorunlu",
  acikOduncler: (k) => db.prepare("SELECT odunc_id, kiraci FROM odunc WHERE kiraci = ? AND durum = 'ACIK'").all(k),
  uyeBul: (k, id) => db.prepare("SELECT uye_id, kiraci, ad FROM uye WHERE kiraci = ? AND uye_id = ?").all(k, id),
};

const KIRACI = "kuzey";                       // istegi yapan gorevlinin kiracisi
const sizan = (satirlar) => satirlar.filter((s) => s.kiraci !== KIRACI).length;

console.log("depo                    sorgu             donen  sizan");
for (const [depo, satirlar] of [
  [gevsek, gevsek.acikOduncler()],
  [siki, siki.acikOduncler(KIRACI)],
]) {
  console.log(`${depo.ad.padEnd(23)} ${"acikOduncler".padEnd(17)} ${String(satirlar.length).padStart(5)} ${String(sizan(satirlar)).padStart(6)}`);
}
for (const [depo, satirlar] of [
  [gevsek, gevsek.uyeBul("U-9")],
  [siki, siki.uyeBul(KIRACI, "U-9")],
]) {
  console.log(`${depo.ad.padEnd(23)} ${"uyeBul('U-9')".padEnd(17)} ${String(satirlar.length).padStart(5)} ${String(sizan(satirlar)).padStart(6)}`);
}

// 3. Kiracinin kaynagi: istekten mi, belirtecten mi?
function baglamKur(kaynak, belirtec, istek) {
  return kaynak === "istek" ? istek.kiraci : belirtec.kiraci;
}
const belirtec = { ozne: "P-2001", kiraci: "kuzey" };
console.log("\nkiraci kaynagi   istekteki deger  kullanilan  donen kayit  sizan");
for (const kaynak of ["istek", "belirtec"]) {
  const istek = { kiraci: "guney", uye_id: "U-9" };       // istemci baska kiraci yaziyor
  const k = baglamKur(kaynak, belirtec, istek);
  const satirlar = siki.uyeBul(k, istek.uye_id);
  console.log(`${kaynak.padEnd(16)} ${istek.kiraci.padEnd(16)} ${k.padEnd(11)} ${String(satirlar.length).padStart(11)} ${String(sizan(satirlar)).padStart(6)}`);
}

// 4. Statik denetim: kiraci sutunu tasiyan tablolara giden sorgularda suzgec var mi?
const KIRACILI_TABLOLAR = ["uye", "odunc"];
const SORGULAR = [
  { ad: "acikOduncler/gevsek", sql: "SELECT odunc_id FROM odunc WHERE durum = 'ACIK'" },
  { ad: "acikOduncler/siki",   sql: "SELECT odunc_id FROM odunc WHERE kiraci = ? AND durum = 'ACIK'" },
  { ad: "uyeBul/siki",         sql: "SELECT ad FROM uye WHERE kiraci = ? AND uye_id = ?" },
  { ad: "sayim",               sql: "SELECT count(*) FROM odunc" },
  { ad: "kitap listesi",       sql: "SELECT isbn FROM kitap" },
];
console.log("\nsorgu                  tablo   kiracili  suzgec  bulgu");
let bulgu = 0;
for (const s of SORGULAR) {
  const tablo = /FROM\s+(\w+)/i.exec(s.sql)?.[1] ?? "-";
  const kiracili = KIRACILI_TABLOLAR.includes(tablo);
  const suzgec = /\bkiraci\s*=\s*\?/i.test(s.sql);
  const sorunlu = kiracili && !suzgec;
  if (sorunlu) bulgu++;
  console.log(`${s.ad.padEnd(22)} ${tablo.padEnd(7)} ${(kiracili ? "evet" : "hayir").padEnd(9)} ${(suzgec ? "var" : "yok").padEnd(7)} ${sorunlu ? "EKSIK SUZGEC" : "-"}`);
}
console.log(`denetim bulgusu: ${bulgu} / ${SORGULAR.length}`);
EOF
node kiraci.mjs
```

```text
depo                    sorgu             donen  sizan
kiraci suzgeci yok      acikOduncler          3      2
kiraci suzgeci zorunlu  acikOduncler          1      0
kiraci suzgeci yok      uyeBul('U-9')         1      1
kiraci suzgeci zorunlu  uyeBul('U-9')         0      0

kiraci kaynagi   istekteki deger  kullanilan  donen kayit  sizan
istek            guney            guney                 1      1
belirtec         guney            kuzey                 0      0

sorgu                  tablo   kiracili  suzgec  bulgu
acikOduncler/gevsek    odunc   evet      yok     EKSIK SUZGEC
acikOduncler/siki      odunc   evet      var     -
uyeBul/siki            uye     evet      var     -
sayim                  odunc   evet      yok     EKSIK SUZGEC
kitap listesi          kitap   hayir     yok     -
denetim bulgusu: 2 / 5
```

#### Unutulan Süzgecin Gözlenebilir Sonucu

Çıktının ilk bölümü sızıntıyı sayıyor. Süzgeçsiz depo, açık ödünçler sorgusunda üç kayıt
döndürüyor ve bunların ikisi başka kiracıya ait. Üye arama sorgusu daha çarpıcıdır:
istenen kimlik komşu kurumun üyesine aittir ve süzgeçsiz sorgu o kaydı döndürür — rol
denetimi geçilmiştir, çünkü görevlinin "üye okuma" izni vardır. İzin doğrudur; yanlış olan,
iznin **hangi satırlar üzerinde** geçerli olduğunun sorulmamasıdır.

Süzgeçli depoda aynı iki sorgu sıfır sızıntı veriyor. İkinci sorgunun sonucu boştur:
kayıt vardır ama bu kiracı için yoktur. Boş sonuç ile yetkisizlik yanıtı arasındaki seçim
bir tasarım kararıdır ve genellikle **boş sonuç** tercih edilir; "bu kimlik başka bir
kurumda var" bilgisi bile kiracılar arası bir sızıntıdır.

#### Kiracı Bilgisi Nereden Gelir

İkinci bölüm daha ince bir hatayı gösteriyor. Kiracı kimliği isteğin gövdesinden ya da yol
parametresinden okunursa, istemci onu değiştirebilir: istek `guney` yazar, sorgu o kiracıyı
kullanır ve komşu kurumun kaydı döner. Aynı istek, kiracı kimliği **belirteçten** alındığında
sıfır kayıt döndürür.

Kural tek cümledir: kiracı kimliği, kimliği doğrulanmış bağlamdan gelir; istekten gelen
kiracı değeri en fazla bir **filtre** olabilir, hiçbir zaman yetkinin kaynağı olamaz.
İstemcinin gönderdiği kiracı ile bağlamdaki kiracı çelişiyorsa istek reddedilir; bu çelişki
aynı zamanda kaydedilmeye değer bir olaydır.

#### Süzgeci Unutmayı Zorlaştırmak

Üçüncü bölüm bir denetim yazıyor: kiracı sütunu taşıyan tablolara giden sorgular süzgeci
içeriyor mu? Modeldeki beş sorgunun ikisi bulgu üretti — biri bilinçli olarak gevşek
bırakılan depo, diğeri masum görünen bir sayım sorgusu. Sayım sorgusu tek bir sayı döndürür
ama o sayı bütün kiracıların toplamıdır; gösterge panosunda bu, komşu kurumun iş hacmini
sızdırır.

Denetimin değeri, unutmayı **yapısal olarak** yakalamasıdır. Aynı işi üç katman birlikte
yapar ve her biri diğerinin kaçırdığını tutar:

1. **Depo katmanı**: kiracı parametresi olmayan bir sorgu yazılamaz — sorguyu kuran işlev
   kiracıyı zorunlu argüman olarak alır.
2. **Denetim betiği**: yeni yazılan sorgular sürekli tümleştirmede taranır, süzgeçsiz
   sorgu derlemeyi düşürür.
3. **Veritabanı katmanı**: satır düzeyi güvenlik politikası, uygulama katmanı hata yapsa
   bile satırları süzer. Bu, İlişkisel Veritabanı Yönetimi kursundaki Satır Düzeyi Güvenlik
   dersinin uygulama tarafındaki karşılığıdır.

Üçü birden gerekir. Yalnız depo katmanına güvenmek, ham sorgu yazan bir raporlama
betiğinde kırılır; yalnız veritabanı katmanına güvenmek, kiracı bağlamını oturuma
bağlamayan bir toplu işte kırılır.

#### Kiracı Verisinin Fiziksel Yerleşimi

Yalıtımın üç yerleşim biçimi vardır ve seçim ölçütü aynı üç soruyla verilir: komşu
kiracının verisine yanlışlıkla erişme olasılığı, kiracı başına maliyet ve göç ile yedeklemenin
tanecikliği.

**Paylaşılan tablo** (bu dersteki model) en ucuzudur ve en çok özen ister: bütün kayıtlar
aynı tablodadır, yalıtımı sorgu sağlar. **Kiracı başına şema**, süzgeci bağlantı düzeyine
taşır; yanlış şemaya bağlanmak hâlâ olanaklıdır ama sorgu içindeki unutkanlık zararsızlaşır.
**Kiracı başına veritabanı** en güçlü yalıtımı verir; bedeli, göç ve yedekleme işlerinin
kiracı sayısıyla çarpılmasıdır.

Karar tek seferlik değildir: küçük kiracılar paylaşılan tabloda, sözleşmesi yalıtım
gerektiren büyük kiracılar ayrı veritabanında tutulabilir. Bu durumda uygulama katmanının
her iki yerleşimi de aynı arayüzün arkasında görmesi gerekir.

#### Özet

- Çok kiracılı sistemde sınır rol ya da kapsamla değil veriyle çizilir; aynı role sahip iki
  görevli farklı satır kümelerini görür.
- Kiracı süzgeci unutulduğunda rol denetimi geçen istek komşu kurumun kaydını döndürür;
  modeldeki iki sorgu üç kayıttan ikisini sızdırdı, süzgeçli sürüm sıfır sızdırdı.
- Kiracı kimliği, kimliği doğrulanmış bağlamdan gelir; istekten gelen değer yetkinin kaynağı
  olamaz — modelde istekten okunan kiracı komşu kurumun kaydını açtı.
- Süzgeci unutmak üç katmanla zorlaştırılır: kiracıyı zorunlu argüman yapan depo, sorguları
  tarayan denetim ve satır düzeyi güvenlik politikası.
- Fiziksel yerleşim (paylaşılan tablo, kiracı başına şema, kiracı başına veritabanı) yalıtım
  gücü ile kiracı başına maliyet arasındaki ödünleşimdir.

#### Sonraki Adım

Kiracı sınırı, isteğin **hangi kümeye** dokunabileceğini belirledi. Küme içinde ise hâlâ
tek tek nesneler vardır: aynı kütüphanenin iki üyesi, birbirinin ödünç kaydını görmemelidir.
Kursun son dersi bu son sınırı ele alır — adres çubuğuna başka bir kimlik yazıldığında ne
olduğunu ve nesne sahipliğinin nerede denetlenmesi gerektiğini.

### Nesne Düzeyi Yetki Denetimi

Source: https://academia.sh/tr/kurslar/kimlik-ve-yetki/nesne-duzeyi-yetki-denetimi

Rol, kapsam ve kiracı denetimlerinden sonra kalan sahiplik sınırı, denetimin sorgu içine konmasıyla dışında bırakılmasının farkı, yanıt kodundan sızan varlık bilgisi ve sahiplik ölçütünün taranabilir hâle getirilmesi.

Kiracı sınırı, isteğin hangi kümeye dokunabileceğini belirledi. Küme içinde ise hâlâ tek
tek nesneler vardır. Aynı kütüphanenin iki üyesi aynı rolü taşır, aynı kapsamlı belirteci
kullanır ve aynı kiracıya aittir; buna rağmen biri diğerinin ödünç kaydını görmemelidir.

Bu son sınır, adres çubuğunda bir kimlik değiştirildiğinde sınanır. Rol denetimi geçilir,
kapsam denetimi geçilir, kiracı denetimi geçilir — ve istek yine de reddedilmelidir, çünkü
istenen **nesne** isteği yapana ait değildir.

#### Üç Gerçekleştirim, Aynı Uç Nokta

Sahiplik denetimi üç yerde yapılabilir: hiç yapılmaz, kayıt çekildikten sonra yapılır ya da
sorgunun kendisine konur. Aşağıdaki hesap üçünü aynı istek kümesiyle çalıştırır.

```bash
cat > nesne.mjs <<'EOF'
// nesne.mjs — nesne duzeyi yetki denetimi: sahiplik sorgunun icinde mi disinda mi
import { DatabaseSync } from "node:sqlite";
import { randomUUID } from "node:crypto";

const db = new DatabaseSync(":memory:");
db.exec(`
  CREATE TABLE odunc (odunc_id TEXT PRIMARY KEY, uye_id TEXT NOT NULL, isbn TEXT NOT NULL,
                      durum TEXT NOT NULL, not_metni TEXT);
  INSERT INTO odunc VALUES
    ('O-1','U-1001','978-0201896831','ACIK','Kapak zedeli teslim alindi'),
    ('O-2','U-1001','978-0262033848','KAPALI',NULL),
    ('O-3','U-1002','978-0131103627','ACIK','Ikinci uyari gonderildi');
`);

// Uc gerceklestirim. Ucu de ayni istegi karsilar: GET /oduncler/{id}
const uclar = {
  denetimsiz: (ctx, id) => db.prepare("SELECT * FROM odunc WHERE odunc_id = ?").all(id),
  cekSonraDenetle: (ctx, id) => {
    const satir = db.prepare("SELECT * FROM odunc WHERE odunc_id = ?").all(id)[0];
    if (!satir) return { durum: 404, govde: null };
    if (satir.uye_id !== ctx.uye) return { durum: 403, govde: { hata: "bu kayit size ait degil" } };
    return { durum: 200, govde: satir };
  },
  sorguDaDenetle: (ctx, id) => {
    const satir = db.prepare("SELECT * FROM odunc WHERE odunc_id = ? AND uye_id = ?").all(id, ctx.uye)[0];
    return satir ? { durum: 200, govde: satir } : { durum: 404, govde: null };
  },
};

const ctx = { uye: "U-1001" };          // istegi yapan uye
const dolgu = (s, n) => String(s).padEnd(n);

console.log("gerceklestirim    istenen  sonuc                       baskasinin kaydi gorundu mu");
for (const id of ["O-1", "O-3", "O-9"]) {
  const ham = uclar.denetimsiz(ctx, id);
  const sahip = ham[0] && ham[0].uye_id !== ctx.uye;
  console.log(`${dolgu("denetimsiz", 17)} ${dolgu(id, 8)} ${dolgu(ham.length ? "200 kayit dondu" : "bos", 27)} ${sahip ? "EVET" : "hayir"}`);
}
for (const [ad, f] of [["cek-sonra-denetle", uclar.cekSonraDenetle], ["sorguda-denetle", uclar.sorguDaDenetle]]) {
  for (const id of ["O-1", "O-3", "O-9"]) {
    const y = f(ctx, id);
    console.log(`${dolgu(ad, 17)} ${dolgu(id, 8)} ${dolgu(y.durum + (y.govde ? " kayit dondu" : " govdesiz"), 27)} hayir`);
  }
}

// Varlik sizintisi: iki gerceklestirimin yanit kodlari baskasinin kaydini ele veriyor mu?
console.log("\nyanit kodlarindan cikarilabilen bilgi");
for (const [ad, f] of [["cek-sonra-denetle", uclar.cekSonraDenetle], ["sorguda-denetle", uclar.sorguDaDenetle]]) {
  const varOlanBaskasinin = f(ctx, "O-3").durum;
  const hicYok = f(ctx, "O-9").durum;
  console.log(`  ${dolgu(ad, 18)} baskasinin kaydi: ${varOlanBaskasinin}   olmayan kayit: ${hicYok}` +
    (varOlanBaskasinin !== hicYok ? "   -> kaydin varligi sizdi" : "   -> ikisi ayirt edilemiyor"));
}

// Tahmin edilemez kimlik yetkilendirme degildir.
const rastgele = randomUUID();
console.log("\ntahmin edilemez kimlik denemesi");
console.log(`  uretilen kimlik: ${rastgele.length} karakter, sozlukten tahmin edilemez`);
console.log("  ama kimlik ele gecerse (kayit, gunluk, paylasilan baglanti) denetimsiz uc nokta yine kaydi doner.");
console.log(`  denetimsiz uc nokta O-3 icin: ${uclar.denetimsiz(ctx, "O-3").length} kayit`);

// Uc nokta taramasi: hangi uc noktalar sahiplik olcutunu bildiriyor?
const UC_NOKTALAR = [
  { yol: "GET /oduncler/{id}",            olcut: "uye_id = :ozne" },
  { yol: "DELETE /oduncler/{id}",         olcut: "uye_id = :ozne" },
  { yol: "GET /oduncler/{id}/not",        olcut: null },
  { yol: "GET /uyeler/{id}/cezalar",      olcut: "uye_id = :ozne" },
  { yol: "POST /oduncler/{id}/uzat",      olcut: null },
  { yol: "GET /raporlar/gecikme",         olcut: "rol = gorevli" },
];
console.log("\nuc nokta                       sahiplik olcutu     bulgu");
let eksik = 0;
for (const u of UC_NOKTALAR) {
  const sorunlu = u.olcut === null;
  if (sorunlu) eksik++;
  console.log(`${dolgu(u.yol, 30)} ${dolgu(u.olcut ?? "-", 19)} ${sorunlu ? "OLCUT BILDIRILMEMIS" : "-"}`);
}
console.log(`bildirilmemis uc nokta: ${eksik} / ${UC_NOKTALAR.length}`);
EOF
node nesne.mjs
```

```text
gerceklestirim    istenen  sonuc                       baskasinin kaydi gorundu mu
denetimsiz        O-1      200 kayit dondu             hayir
denetimsiz        O-3      200 kayit dondu             EVET
denetimsiz        O-9      bos                         hayir
cek-sonra-denetle O-1      200 kayit dondu             hayir
cek-sonra-denetle O-3      403 kayit dondu             hayir
cek-sonra-denetle O-9      404 govdesiz                hayir
sorguda-denetle   O-1      200 kayit dondu             hayir
sorguda-denetle   O-3      404 govdesiz                hayir
sorguda-denetle   O-9      404 govdesiz                hayir

yanit kodlarindan cikarilabilen bilgi
  cek-sonra-denetle  baskasinin kaydi: 403   olmayan kayit: 404   -> kaydin varligi sizdi
  sorguda-denetle    baskasinin kaydi: 404   olmayan kayit: 404   -> ikisi ayirt edilemiyor

tahmin edilemez kimlik denemesi
  uretilen kimlik: 36 karakter, sozlukten tahmin edilemez
  ama kimlik ele gecerse (kayit, gunluk, paylasilan baglanti) denetimsiz uc nokta yine kaydi doner.
  denetimsiz uc nokta O-3 icin: 1 kayit

uc nokta                       sahiplik olcutu     bulgu
GET /oduncler/{id}             uye_id = :ozne      -
DELETE /oduncler/{id}          uye_id = :ozne      -
GET /oduncler/{id}/not         -                   OLCUT BILDIRILMEMIS
GET /uyeler/{id}/cezalar       uye_id = :ozne      -
POST /oduncler/{id}/uzat       -                   OLCUT BILDIRILMEMIS
GET /raporlar/gecikme          rol = gorevli       -
bildirilmemis uc nokta: 2 / 6
```

#### Denetimsiz Uç Noktanın Gözlenebilir Sonucu

İlk bölümdeki tabloda denetimsiz gerçekleştirim, başkasına ait `O-3` kaydını olduğu gibi
döndürüyor. İstek geçerli bir belirteçle geldi, üye kendi hesabındaydı, yalnız yol
parametresi değişti. Sızan şey bir ödünç kaydı; içinde başka bir üyenin okuma geçmişi ve
kaydın not alanı var.

Bu kusur sınıfı, uygulamanın hiçbir katmanında hata üretmediği için sessizdir: günlükte
başarılı bir istek görünür, izleme panosunda hata oranı değişmez. Ancak yakalanması da
kolaydır — her uç nokta için "bu kaydın isteği yapanla ilişkisi nedir" sorusunun yazılı bir
yanıtı varsa, yanıtı olmayan uç noktalar listelenebilir.

#### Denetimin Yeri Yanıtı Değiştirir

İkinci bölüm iki doğru gerçekleştirimi ayırıyor. Kayıt çekildikten sonra denetleyen sürüm,
başkasının kaydı için `403`, olmayan kayıt için `404` döndürüyor. İkisi farklı olduğu için
bir gözlemci, denemediği bir kimliğin **var olduğunu** öğrenebilir: `403` "bu kayıt var ama
senin değil" demektir.

Sorgunun içinde denetleyen sürümde iki durum aynı yanıtı üretir. Kayıt ya isteği yapanın
kümesindedir ya da yoktur; ikisi arasında ayrım yapılmaz. Bu, varlık bilgisinin
sızmamasını sağlar ve ek bir yararı vardır: denetim unutulamaz, çünkü ölçüt sorgunun
parçasıdır. Çek-sonra-denetle kalıbında `if` satırını silmek yeterlidir; sorguda denetleme
kalıbında aynı hatayı yapmak için sorguyu yeniden yazmak gerekir.

Seçim her zaman `404` yönünde değildir. Kullanıcının erişimi olmadığını **bilmesi gereken**
durumlarda — bir ekip üyesinin başka bir ekibin panosuna gitmesi gibi — `403` daha
yardımcıdır ve varlık bilgisi zaten paylaşılan bir bilgidir. Ölçüt şudur: kaydın varlığı
isteği yapan için gizli bir bilgi mi?

#### Tahmin Edilemez Kimlik Bir Yetki Denetimi Değildir

Üçüncü bölüm sık rastlanan bir yanılgıyı ölçüyor. Sıralı sayı yerine tahmin edilemez bir
kimlik kullanmak, kimlikleri **keşfetmeyi** zorlaştırır; kaydı **korumaz**. Kimlik bir
kayıt bağlantısında, tarayıcı geçmişinde, sunucu günlüğünde ya da paylaşılan bir bağlantıda
ele geçtiğinde denetimsiz uç nokta kaydı yine döndürür — çıktının son satırı bunu gösteriyor.

Tahmin edilemez kimliğin yeri savunmanın **derinliğidir**, yerine geçmesi değil. Kimlik
tasarımı toplu keşfi engeller; yetki denetimi tekil erişimi engeller. İkisi farklı işler
yapar ve biri diğerini gereksizleştirmez.

#### Ölçütü Yazılı Hâle Getirmek

Son bölüm uç noktaları tarıyor: her uç nokta için sahiplik ölçütü bildirilmiş mi? Altı uç
noktanın ikisi ölçüt bildirmiyor ve ikisi de alt kaynak: bir ödünç kaydının notu ve uzatma
eylemi. Alt kaynaklar bu kusurun en sık göründüğü yerdir, çünkü üst kaynak için yazılmış
denetimin alt yola da uygulandığı varsayılır.

Ölçütün yazılı olması iki şey sağlar. Yeni bir uç nokta eklendiğinde ölçüt alanı boş
kalırsa tarama bunu bulur — kod incelemesinde gözden kaçan şey listede görünür. İkincisi,
ölçüt bir veri olduğu için sınanabilir: her uç nokta için "sahibi olmayan bir özneyle çağır
ve boş sonuç bekle" testi kendiliğinden üretilebilir.

Bu kursta kurulan bütün modeller — rol, öznitelik, ilişki, politika, kapsam, kiracı —
kararı **nesneden bağımsız** verir. Nesne düzeyi denetim, o kararın son adımıdır ve
atlandığında diğerlerinin hepsi geçilmiş olur. Bu yüzden yetki denetimi tek bir yerde değil,
isteğin yolu boyunca katman katman yapılır.

#### Özet

- Nesne düzeyi denetim, rol, kapsam ve kiracı denetimlerinin hepsi geçildikten sonra kalan
  son sınırdır: istenen kaydın isteği yapanla ilişkisi.
- Denetimsiz uç nokta hata üretmeden başkasının kaydını döndürür; modelde tek bir yol
  parametresi değişikliği yeterli oldu.
- Sahiplik ölçütü sorgunun içine konduğunda hem unutulması zorlaşır hem de varlık bilgisi
  sızmaz; çek-sonra-denetle kalıbında `403` ile `404` ayrımı kaydın varlığını ele verir.
- Tahmin edilemez kimlik keşfi zorlaştırır, erişimi yetkilendirmez; kimlik ele geçtiğinde
  denetimsiz uç nokta kaydı yine döndürür.
- Her uç nokta için sahiplik ölçütünün yazılı olması taranabilir ve sınanabilir kılar;
  modelde ölçütü bildirilmemiş iki uç noktanın ikisi de alt kaynaktı.

#### Kurs Kapanışı

Bu kurs iki soruyu birbirinden ayırarak başladı: **kim olduğun** ve **ne yapabildiğin**.
Kimlik Doğrulama Akışları konusu birinci soruyu ele aldı; temel şemadan oturum ve belirteç
tabanlı akışlara, imza doğrulamanın ayrıntısına, yenileme belirteçlerinin dönüşümlü
kullanımına, yetki devri protokollerine ve çok etmenli doğrulamaya kadar her akışın hangi
sorunu çözdüğünü ve nerede kırıldığını gösterdi. Kimlik Bilgisi Güvenliği konusu
kimlik bilgisinin saklanmasını ölçülebilir kurallara bağladı: yavaş karma parametreleri,
tuzun ve biberin ayrı işleri, sıfırlama akışının yüzeyi ve oturumun ömrü. Yetkilendirme
Modelleri konusu ikinci soruyu dört modelde kurdu, kapsamla istemci yetkisini daralttı,
kiracı sınırını sorguya taşıdı ve bu dersle nesne düzeyine indi.

Kurs boyunca değişmeyen üç ölçüt vardı. Karar bir yerde **verilir**, başka bir yerde
**uygulanır**; ikisinin ayrılması denetimi mümkün kılar. Reddin nedeni ayırt edilebilir
olmalıdır, ama ayrıntısı isteği yapana sızmamalıdır. Ve her denetim, unutulduğunda
gözlenebilir bir sonuç üretmelidir — sızan kayıt sayısı, açılan uç nokta sayısı, ayırt
edilebilen yanıt kodu.

Sıradaki kurs, **Veri Erişim Katmanı ve İş Mantığı**, bu kararların altındaki katmana
iner. Yetki denetiminin sorguya taşınması bu kursta bir güvenlik gereği olarak çıktı;
orada aynı sorgu bir başarım ve tasarım sorusu hâline gelir: veri erişimi iş mantığından
nasıl ayrılır, işlem sınırı nereye çizilir ve aynı veriye giden yolların maliyeti nasıl
ölçülür?

