İçeriğe geç
academia.sh

Ders 05 / 23

JSON Web Belirteçleri

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

İçindekiler

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

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

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

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

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

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

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

Notlarım

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

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat