---
title: 'OpenID Connect'
source: 'https://academia.sh/tr/kurslar/kimlik-ve-yetki/openid-connect'
course: 'Kimlik Doğrulama ve Yetkilendirme'
language: tr
updated: '2026-08-17T18:06:48+00:00'
license: 'CC BY-SA 4.0'
---

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