Mert Özen Her Satırda Daha İleri
Güvenlik Authentication ve Authorization — #12

OpenID Connect: ID Token, UserInfo, Discovery, ASP.NET Core'da AddOpenIdConnect ve Logout Türleri

Mert Özen 8 Kas 2026 12 dk 5 görüntülenme
OpenID Connect: ID Token, UserInfo, Discovery, ASP.NET Core'da AddOpenIdConnect ve Logout Türleri

ID token client için yazılmış bir giriş kaydıdır, access token ise API için bir yetki belgesi; ikisini karıştırmak güvenlik açığıdır. Keycloak lab'ında discovery, JWKS ve UserInfo'yu okuyor, AddOpenIdConnect'te MapInboundClaims'in claim isimlerini nasıl değiştirdiğini gösteriyor, RP-initiated logout'u izliyor ve back-channel logout endpoint'ini yazıp başka bir uygulamadan çıkışın bu uygulamadaki cookie'yi de düşürdüğünü doğruluyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10, Microsoft.AspNetCore.Authentication.OpenIdConnect 10.0.14 ve Keycloak 26.8.2 ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore'un web uygulaması Keycloak'la giriş yapıyor. User.FindFirst("sub") null dönüyor, oysa ID token'da sub olduğunu biliyorsun. Neden? İkinci soru: Ada uygulamadan çıkış yaptı; uygulamanın cookie'si silindi. Ada "Giriş yap"a tekrar bastığında parola sorulur mu? Üçüncüsü: Ada aynı Keycloak oturumuyla girdiği başka bir Bookstore uygulamasından çıkış yaparsa, bu web uygulamasındaki oturumu ne olur?

Üç sorunun da cevabı OIDC'nin (OpenID Connect) OAuth üzerine eklediği katmanda. 10. yazıda OAuth'un bir authentication protokolü olmadığını, 11. yazıda code'un token'a nasıl çevrildiğini gördük. Bu yazıda OIDC'nin getirdiklerini, yani ID token'ı, UserInfo'yu ve discovery'yi, ASP.NET Core'un AddOpenIdConnect'inin bunları nasıl işlediğini ve en çok ihmal edilen konuyu, logout'u inceliyoruz. Lab önceki yazıyla aynı: Keycloak'ta bookstore realm'i ve confidential client bookstore-web.

ID Token ile Access Token: Kime Yazılmış?

Önceki yazıda token endpoint'inden iki JWT (JSON Web Token) birlikte geldi. İkisi de Keycloak'un aynı anahtarıyla imzalı, ikisinde de sub var; ama okuyucuları farklı:

ID tokenAccess token
Kim içinClient (aud = bookstore-spa)Resource server (aud = bookstore-api)
Neyi anlatırBir giriş olayı: kim, ne zaman, nasılBir yetki: hangi scope'larla hangi API'ye (Application Programming Interface)
Kim doğrularClient, her alanıylaAPI; client için opaque kabul edilir
Nereye gönderilirHiçbir yere (yalnız logout'ta id_token_hint)Authorization: Bearer header'ıyla API'ye
StandartOIDC Core 1.0OAuth 2.0; JWT profili RFC (Request for Comments) 9068

ID token'ın claim'leri giriş olayını anlatıyor: iss (kim üretti), sub (kullanıcının issuer içindeki kalıcı kimliği), aud (hangi client için), exp/iat, auth_time (kullanıcı en son ne zaman gerçekten kimlik doğruladı), nonce, acr (doğrulamanın seviyesi) ve varsa amr (kullanılan yöntemler). sid ise Keycloak'taki session'ın kimliği; logout'ta lazım olacak. 05. yazıdaki step-up kararını acr ve auth_time üzerinden verdiğimizi hatırla.

Kullanıcının kimliğini her zaman iss + sub çiftiyle tanı. E-posta değişir, iki farklı IdP'de (Identity Provider) aynı e-posta iki farklı kişiye ait olabilir; 04. yazıdaki "aynı e-postayla iki hesap" sorununun kaynağı bu.

Discovery ve JWKS

Client'ın Keycloak hakkında bilmesi gereken her şey tek bir adreste:

$ curl -s http://localhost:19200/realms/bookstore/.well-known/openid-configuration | jq '{issuer, authorization_endpoint, ...}'
{
  "issuer": "http://localhost:19200/realms/bookstore",
  "authorization_endpoint": "http://localhost:19200/realms/bookstore/protocol/openid-connect/auth",
  "token_endpoint": "http://localhost:19200/realms/bookstore/protocol/openid-connect/token",
  "userinfo_endpoint": "http://localhost:19200/realms/bookstore/protocol/openid-connect/userinfo",
  "end_session_endpoint": "http://localhost:19200/realms/bookstore/protocol/openid-connect/logout",
  "jwks_uri": "http://localhost:19200/realms/bookstore/protocol/openid-connect/certs",
  "id_token_signing_alg_values_supported": [
    "PS384",
    "RS384",
    "EdDSA",
    "ES384",
    "HS256",
    "HS512",
    "ES256",
    "RS256",
    "HS384",
    "ES512",
    "PS256",
    "PS512",
    "RS512"
  ],
  "subject_types_supported": [
    "public",
    "pairwise"
  ],
  "backchannel_logout_supported": true,
  "backchannel_logout_session_supported": true,
  "frontchannel_logout_supported": true
}

Authority'yi verdiğinde ASP.NET Core'un yaptığı ilk şey bu dokümanı indirmek; log'daki "Updating configuration" satırı o. Ardından jwks_uri'deki public key'leri alıyor:

$ curl -s http://localhost:19200/realms/bookstore/protocol/openid-connect/certs | jq '.keys[] | {kid, kty, alg, use, n: (.n[0:16]+"..."), e}'
{
  "kid": "3xDZ-2W6Nlja7hkWMS5oNwllmASgYJ8e3OK9yN82h9Y",
  "kty": "RSA",
  "alg": "RSA-OAEP",
  "use": "enc",
  "n": "jU3lpk0IvCg6jl3X...",
  "e": "AQAB"
}
{
  "kid": "-bTt6-Fb7_swq5REoKGPi3jdsUdOOwmbRw1ZG2hLKAY",
  "kty": "RSA",
  "alg": "RS256",
  "use": "sig",
  "n": "3UQHpYVHPxAAKmTw...",
  "e": "AQAB"
}

İki key var: use: sig olan imza doğrulamak için, use: enc olan ise token'ları şifrelemek (JWE, JSON Web Encryption) isteyen client'lar için. ID token'ın header'ındaki kid imza key'iyle eşleşiyor. 08. yazıda kendi JWKS (JSON Web Key Set) endpoint'imizi yazıp key rotation'ı elle yapmıştık; burada aynı mekanizmayı authorization server yönetiyor, handler da bilinmeyen bir kid gördüğünde konfigürasyonu yeniden çekiyor. Bir ayrıntı: id_token_signing_alg_values_supported listesinde HS256 (Hash-based Message Authentication Code ile, paylaşılan bir secret'la imza) da var. Keycloak'ta client bazında imza algoritması seçilebiliyor; handler'ın kabul ettiği algoritmaları TokenValidationParameters.ValidAlgorithms ile daraltmak bu yüzden hâlâ iyi bir fikir.

Bu belgeler "OpenID Connect Discovery 1.0" ve "OpenID Connect Core 1.0". Core'la birlikte dokuz OIDC spesifikasyonu 2024'te ISO (International Organization for Standardization)/IEC (International Organization for Standardization / International Electrotechnical Commission) standardı olarak da yayımlandı; Core'un numarası ISO/IEC 26131:2024. Kurumsal satın alma dokümanlarında bu numarayı görürsen kastedilen OIDC Core.

UserInfo: Token'a Sığmayanlar

UserInfo endpoint'i, access token'la çağrılan ve kullanıcının claim'lerini JSON (JavaScript Object Notation) olarak dönen bir OIDC endpoint'i:

$ curl -s $B/userinfo -H "Authorization: Bearer $AT" | jq .
{
  "sub": "813c410a-8e19-4979-85aa-5cc46f3cb5ab",
  "email_verified": true,
  "name": "Ada Lovelace",
  "preferred_username": "ada@bookstore.test",
  "given_name": "Ada",
  "family_name": "Lovelace",
  "email": "ada@bookstore.test"
}
$ curl -s -D - -o /dev/null $B/userinfo -H "Authorization: Bearer $IDT"
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="bookstore", error="invalid_token", error_description="Token verification failed"

İkinci istek ID token'ı access token yerine göndermeyi deniyor ve reddediliyor. İyi; ID token bir API'nin anahtarı değil. ID token'ı küçük tutup büyük ya da sık değişen claim'leri (adres, profil fotoğrafı, uzun grup listeleri) UserInfo'dan almak cookie boyutunu da küçültür.

AddOpenIdConnect ve Claim Mapping

Lab'daki web uygulamasının OIDC ayarları:

.AddOpenIdConnect(options =>
{
    options.Authority = builder.Configuration["Oidc:Authority"];
    options.RequireHttpsMetadata = false; // yalnız yerel lab
    options.ClientId = builder.Configuration["Oidc:ClientId"];
    options.ClientSecret = builder.Configuration["Oidc:ClientSecret"];
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.Scope.Add("orders.read");
    options.MapInboundClaims = builder.Configuration.GetValue("Oidc:MapInboundClaims", true);
    options.GetClaimsFromUserInfoEndpoint = builder.Configuration.GetValue("Oidc:UserInfo", false);
    options.ClaimActions.Remove("acr"); // acr varsayılan olarak silinir; step-up için tutuyoruz
    options.TokenValidationParameters.NameClaimType = "name";
    options.SaveTokens = true;
});

app.MapGet("/claims", (ClaimsPrincipal user) =>
    user.Claims.Select(c => $"{c.Type} = {c.Value}")).RequireAuthorization();

Scope listesi varsayılan olarak openid ve profile içeriyor; ben orders.read'i ekledim. MapInboundClaims'in varsayılanı true. Bu ayarla giriş yapıp /claims'e bakınca:

auth_time = 1794116170
jti = 8f748bff-df85-f6fb-712a-5ded33e294e2
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier = 813c410a-8e19-4979-85aa-5cc46f3cb5ab
typ = ID
sid = E5BxuRtA1R8W44aQ8d0aDVFo
http://schemas.microsoft.com/claims/authnclassreference = 1
email_verified = true
name = Ada Lovelace
preferred_username = ada@bookstore.test
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname = Ada
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname = Lovelace
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress = ada@bookstore.test

Önce Tahmin Et'in ilk sorusunun cevabı burada: sub yok, yerinde Web Services Federation (WS-Federation) döneminden kalma uzun bir URI (Uniform Resource Identifier) var. Token handler, bilinen kısa JWT claim isimlerini .NET'in eski ClaimTypes sabitlerine çeviriyor; acr bile authnclassreference olmuş. ClaimTypes.NameIdentifier ile çalışan eski kod için kolaylık, ama token'daki isimle koddaki isim ayrışıyor, log'lar okunmaz hâle geliyor ve başka bir handler (ör. JWT Bearer) farklı ayarlıysa aynı kullanıcı iki farklı claim setiyle görünüyor. Oidc:MapInboundClaims=false ile aynı giriş:

auth_time = 1794116182
jti = ffe7d692-f882-5794-aa44-15925343e94b
sub = 813c410a-8e19-4979-85aa-5cc46f3cb5ab
typ = ID
sid = yt52UejvYYsurMpluaCEvXXg
acr = 1
email_verified = true
name = Ada Lovelace
preferred_username = ada@bookstore.test
given_name = Ada
family_name = Lovelace
email = ada@bookstore.test

Token'da ne yazıyorsa o. MapInboundClaims = false verince User.Identity.Name'in hangi claim'den okunacağını da söylemen gerekiyor; NameClaimType = "name" satırı bunun için. Rol kullanıyorsan RoleClaimType da aynı şekilde.

Listede olmayanlar da öğretici: iss, aud, exp, nonce, azp, at_hash. Handler'ın ClaimActions listesi bunları varsayılan olarak siliyor; ASP.NET Core kaynak kodundaki OpenIdConnectOptions constructor'ında acr da bu listede. Doğrulama için gerekliydiler ama cookie'de yer kaplamaları gereksiz. Step-up için acr lazım olduğundan ClaimActions.Remove("acr") ile silme kuralını kaldırdım; çıktıda acr = 1 bu yüzden duruyor.

GetClaimsFromUserInfoEndpoint = true verince handler code'u değiştirdikten sonra UserInfo'yu da çağırıyor; Trace seviyesindeki log:

trce: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[30]
      Token response received.
trce: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[20]
      Retrieving claims from the user info endpoint.
trce: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[35]
      User information received: System.Text.Json.JsonDocument

Dikkat: UserInfo cevabından yalnız ClaimActions'da eşlemesi olan key'ler principal'a geçiyor. Varsayılan eşlemeler sub, name, given_name, family_name, profile ve email; başka bir alan istiyorsan options.ClaimActions.MapUniqueJsonKey("preferred_username", "preferred_username") gibi kendin eklemelisin. Handler ayrıca UserInfo'daki sub'ın ID token'daki sub'la aynı olduğunu kontrol ediyor; farklıysa girişi reddediyor.

SaveTokens = true access, refresh ve ID token'ı authentication cookie'sinin içine koyuyor; await HttpContext.GetTokenAsync("access_token") ile okunuyor. Bunun bedeli cookie boyutu: önceki yazıdaki cookie iki chunk'a bölünmüştü. Token'ları cookie yerine sunucu tarafında tutmak (02. yazıdaki ITicketStore) BFF (Backend for Frontend) yazısında yeniden karşımıza çıkacak.

Logout 1: RP-Initiated

Logout'un zorluğu şu: ortada iki session var. Uygulamanın cookie'si ve Keycloak'un SSO (Single Sign-On) session'ı. Yalnız uygulamanın cookie'sini silersen, Ada "Giriş yap"a bastığında Keycloak onu parola sormadan geri gönderir; Önce Tahmin Et'in ikinci sorusunun cevabı bu. RP (Relying Party)-initiated logout (OpenID Connect RP-Initiated Logout 1.0; RP, Relying Party, yani client) iki scheme'den birden çıkmak demek:

app.MapPost("/logout", () => Results.SignOut(
    new AuthenticationProperties { RedirectUri = "/signed-out" },
    [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme]));
$ sh logout12.sh
callback 302 http://localhost:19212/claims
1) 302 http://localhost:19200/realms/bookstore/protocol/openid-connect/logout?post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A19212%2Fsignout-callback-oidc&id_token_hint=eyJ...&state=CfDJ8...&x-client-SKU=ID_NET10_0&x-client-ver=8.19.2.0
2) 302 http://localhost:19212/signout-callback-oidc?state=CfDJ8...
3) 302 http://localhost:19212/signed-out
4) Signed out.

Birinci adımda cookie handler cookie'yi siliyor, OIDC handler tarayıcıyı end_session_endpoint'e gönderiyor. id_token_hint Keycloak'a "bu session'ı, bu client için ben başlatmıştım" diyor; o olmadan Keycloak 26 bir onay sayfası gösteriyor. post_logout_redirect_uri client'ta kayıtlı olmalı (post.logout.redirect.uris); redirect URI'deki birebir eşleşme kuralı burada da geçerli. İkinci adımda Keycloak SSO session'ını bitirip tarayıcıyı geri gönderiyor, handler state'ten RedirectUri'yi çıkarıyor. Logout'u POST yaptım: GET bir logout endpoint'ini başka bir site bir img etiketiyle tetikleyebilir. Gerçek bir sayfada bu form antiforgery token'ıyla gönderilmeli.

Logout 2: Front-Channel ve Back-Channel

RP-initiated logout yalnız çıkışı başlatan uygulamayı ve Keycloak'u kapsıyor. Ada aynı SSO session'ıyla Bookstore'un üç uygulamasına girmişse diğer ikisinin cookie'leri yaşamaya devam ediyor. Keycloak'un diğer client'lara haber vermesinin iki yolu var.

Front-channel logout (OpenID Connect Front-Channel Logout 1.0) bunu tarayıcı üzerinden yapıyor: Keycloak'un logout sayfası her client'ın logout adresini görünmez bir iframe'de açıyor ve client kendi cookie'sini siliyor. Tarayıcılar üçüncü taraf cookie'lerini engelledikçe iframe'deki istekte client'ın cookie'si gönderilmiyor ve bu yöntem sessizce çalışmayı bırakıyor. Bu yüzden lab'daki client'larda frontchannelLogout kapalı.

Back-channel logout (OpenID Connect Back-Channel Logout 1.0) tarayıcıyı hiç kullanmıyor: Keycloak her client'ın kayıtlı adresine sunucudan sunucuya, imzalı bir logout_token POST ediyor. bookstore-web'in kaydında adres http://host.docker.internal:19212/backchannel-logout; Keycloak container'ı uygulamaya bu adla ulaşıyor. Endpoint:

app.MapPost("/backchannel-logout", async (HttpRequest request, IOptionsMonitor<OpenIdConnectOptions> oidcOptions,
    RevokedSessions revoked, ILogger<Program> logger) =>
{
    IFormCollection form = await request.ReadFormAsync();
    string? logoutToken = form["logout_token"];
    if (string.IsNullOrEmpty(logoutToken))
    {
        return Results.BadRequest();
    }

    OpenIdConnectOptions options = oidcOptions.Get(OpenIdConnectDefaults.AuthenticationScheme);
    OpenIdConnectConfiguration config = await options.ConfigurationManager!.GetConfigurationAsync(request.HttpContext.RequestAborted);
    TokenValidationResult result = await new JsonWebTokenHandler().ValidateTokenAsync(logoutToken, new TokenValidationParameters
    {
        ValidIssuer = config.Issuer,
        ValidAudience = options.ClientId,
        IssuerSigningKeys = config.SigningKeys,
        ValidTypes = ["logout+jwt"]
    });

    if (!result.IsValid)
    {
        logger.LogWarning("Rejected logout token: {Error}", result.Exception?.Message);
        return Results.BadRequest();
    }

    // OIDC Back-Channel Logout 1.0, 2.6: events claim'i olmalı, nonce olmamalı; iptal sid ile yapıldığı için sid şart
    string? sid = result.Claims.TryGetValue("sid", out object? sidValue) ? sidValue as string : null;
    if (!result.Claims.TryGetValue("events", out object? events)
        || !events.ToString()!.Contains("http://schemas.openid.net/event/backchannel-logout")
        || result.Claims.ContainsKey("nonce")
        || sid is null)
    {
        logger.LogWarning("Rejected logout token: required claims missing");
        return Results.BadRequest();
    }

    logger.LogDebug("Logout token claims: {Claims}", JsonSerializer.Serialize(result.Claims));
    revoked.Add(sid);
    logger.LogInformation("Back-channel logout for sid {Sid}", sid);
    return Results.Ok();
}).DisableAntiforgery();

Logout token, discovery'den gelen aynı key'lerle doğrulanıyor: issuer Keycloak, audience bu client, header'daki typ logout+jwt. nonce bulunmaması kuralı, bir ID token'ın logout token gibi kullanılmasını engellemek için. İlk sürümde IsValid kontrolünü claim kontrolleriyle aynı if'e koymuştum; geçersiz bir token'da result.Claims boş geldiği için ID token gönderen bir denemede endpoint 500 döndü. Doğrulamayı ayrı bir adım yapınca:

$ curl -s -o /dev/null -w "%{http_code}\n" -d "logout_token=$IDT" http://localhost:19212/backchannel-logout
400
warn: Program[0]
      Rejected logout token: IDX10214: Audience validation failed. See https://aka.ms/identitymodel/app-context-switches

Endpoint'in tek işi hangi session'ın bittiğini kaydetmek; cookie'ler tarayıcıda, sunucu onlara ulaşamıyor. Asıl iptal, cookie her istekte doğrulanırken oluyor:

.AddCookie(options =>
{
    options.Events.OnValidatePrincipal = context =>
    {
        // Back-channel logout ile iptal edilen IdP session'ına ait cookie'yi reddet
        RevokedSessions revoked = context.HttpContext.RequestServices.GetRequiredService<RevokedSessions>();
        string? sid = context.Principal?.FindFirstValue("sid");
        if (sid is not null && revoked.Contains(sid))
        {
            context.RejectPrincipal();
            return context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
        return Task.CompletedTask;
    };
})

Senaryo: Ada web uygulamasına giriyor, sonra aynı tarayıcıda bookstore-spa'ya giriyor (Keycloak parola sormadan code veriyor; SSO) ve SPA'dan (Single-Page Application) çıkış yapıyor:

$ sh bclogout12.sh
callback 302 http://localhost:19212/claims
web /claims: 200
spa authorize (SSO, no password): http://localhost:19211/callback?state=s2&session_state=pvAV4...
spa logout: 302 http://localhost:19211/
web /claims: 302 http://localhost:19200/realms/bookstore/protocol/openid-connect/auth?client_id=booksto...

dbug: Program[0]
      Logout token claims: {"exp":1794116412,"iat":1794116292,"jti":"a88d91b0-d963-f093-7e9c-13621f6df135","iss":"http://localhost:19200/realms/bookstore","aud":"bookstore-web","sub":"813c410a-8e19-4979-85aa-5cc46f3cb5ab","typ":"Logout","sid":"pvAV4sgJFPyvJR7cS1ZeyIQl","events":{"http://schemas.openid.net/event/backchannel-logout":{}}}
info: Program[0]
      Back-channel logout for sid pvAV4sgJFPyvJR7cS1ZeyIQl

Web uygulamasının cookie'si hâlâ tarayıcıda, ama bir sonraki istekte OnValidatePrincipal onu reddetti ve kullanıcı yeniden Keycloak'a gönderildi. Önce Tahmin Et'in üçüncü sorusunun cevabı: back-channel logout kuruluysa o oturum da biter.

Üretimde Dikkat

RevokedSessions lab için bellekte. Birden fazla instance varsa iptal listesi paylaşılmalı (Redis gibi bir distributed cache) ve kayıtlar en az cookie ömrü kadar tutulmalı; daha kısa tutarsan iptal edilmiş bir cookie listeden düştüğü anda yeniden geçerli olur. Daha temiz yol 02. yazıdaki ITicketStore: session sunucuda tutuluyorsa back-channel logout doğrudan o kaydı siler.

Back-channel endpoint'i authorization server'ın ulaşabileceği bir adreste olmalı; uygulama bir iç ağdaysa Keycloak'tan o adrese erişimi ağ ekibiyle konuş. Uygulama kapalıyken aynı senaryoyu denedim: Keycloak çıkışı yine tamamlayıp 302 döndü, log'a yalnız bir uyarı yazdı:

WARN  [org.keycloak.services] KC-SERVICES0057: Logout for client 'bookstore-web' failed: org.apache.http.conn.HttpHostConnectException: Connect to host.docker.internal:19212 [host.docker.internal/192.168.65.254] failed: Connection refused

Log'da bir tekrar deneme görünmüyor ve client teslim edilemeyen bir logout'tan haberdar olamaz; kaçırılan logout'a karşı tek savunma kısa cookie ömrü ve periyodik yeniden doğrulama. Bu uyarıyı log'larında alarm olarak izle. OnValidatePrincipal'ı her istekte ağır bir işe dönüştürme: bellek ya da cache'ten tek bir lookup yeterli.

Discovery dokümanı handler tarafından cache'leniyor (ConfigurationManager'ın varsayılan yenileme aralığı 12 saat); Keycloak key'ini değiştirirse handler bilinmeyen kid'de yenilemeyi kendisi tetikliyor. Bu yüzden authorization server'ın key rotation'ında eski key'i bir süre JWKS'te bırakması önemli; 08. yazıdaki kural aynen geçerli.

Sık Düşülen Tuzaklar

Birincisi, ID token'ı API'ye göndermek. Yukarıda Keycloak'un kendi UserInfo endpoint'i bile reddetti; kendi API'n ID token'ı kabul ediyorsa audience doğrulaması yanlış demektir. İkincisi, MapInboundClaims'i bir handler'da kapatıp diğerinde açık bırakmak; aynı kullanıcı için sub bir yerde var, bir yerde yok. Üçüncüsü, kullanıcıyı e-postayla eşlemek; doğru anahtar iss + sub.

Dördüncüsü, logout'u yalnız SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme) ile yapmak ve SSO session'ını açık bırakmak; ortak bilgisayardaki bir sonraki kullanıcı "Giriş yap"a basınca önceki kullanıcı olarak içeri girer. Beşincisi, logout'u GET endpoint'i yapmak. Altıncısı, back-channel endpoint'inde logout token'ı doğrulamadan sid'i okumak; o zaman herkes herkesi sistemden atabilir.

Küçük Bir Deneme

★☆☆ MapInboundClaims = false ile NameClaimType satırını sil ve User.Identity.Name'in değerini yazdır. Neden null geldiğini handler'ın varsayılan NameClaimType'ına bakarak açıkla.

★★☆ Keycloak'ta bookstore-web için ID token'a amr claim'ini ekleyen bir mapper tanımla, ClaimActions'a gerekiyorsa eşlemesini ekle ve /claims'te gör. 05. yazıdaki step-up policy'sini bu claim'le çalıştır.

★★★ RevokedSessions'ı IDistributedCache üzerine taşı, uygulamayı iki portta iki instance olarak çalıştır ve back-channel logout'un hangisine gelirse gelsin iki instance'taki cookie'yi de düşürdüğünü göster.

Kullanıcının kimliğini ve oturumunu uçtan uca yönettik. Bir sonraki yazıda API tarafına geçiyoruz: AddJwtBearer ile access token doğrulamak, API başına audience, scope'tan policy'ye eşleme, client credentials token'ını cache'leyen bir DelegatingHandler ve çalınan bearer token'ı işe yaramaz kılan DPoP.