Authorization Code Flow + PKCE Adım Adım: state, nonce, PAR ve Keycloak ile HTTP Trafiğini Okumak
PKCE, front channel'dan geçen code'u onu isteyen client'a bağlar; çalınan code verifier olmadan işe yaramaz. Keycloak lab'ında akışı curl'le adım adım okuyor, state ve nonce'un neyi engellediğini, yanlış verifier'dan sonra code'un yandığını ve redirect URI'nin neden birebir eşleşmesi gerektiğini gösteriyor; sonra PAR ile parametreleri back channel'a taşıyıp ASP.NET Core'un OIDC handler'ını uçtan uca izliyoruz.
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
Ada, Bookstore'a Keycloak üzerinden giriş yapıyor. Tarayıcı geri döndüğünde adres çubuğunda bir code var. Bu code'u bir saldırgan ele geçirse, örneğin bir tarayıcı eklentisi ya da proxy log'u üzerinden, onunla token alabilir mi? Peki code'u yanlış bir code_verifier'la bir kez denese ve başarısız olsa, gerçek client aynı code'u hâlâ kullanabilir mi? Bir de ASP.NET Core tarafı: giriş dönüşündeki state parametresinin bir karakterini değiştirirsen uygulama ne yapar?
Bir önceki yazıda authorization code grant'in mantığını kurduk. Bu yazıda akışın içine giriyoruz: Keycloak'a curl'le kendimiz gidecek, her isteği ve cevabı okuyacak, sonra aynı akışı ASP.NET Core'un OIDC (OpenID Connect) handler'ına bırakıp onun neyi bizim yerimize yaptığını göreceğiz. Bütün denemeler kendi makinemdeki lab'da; hiçbir şey dışarıdaki bir sisteme gönderilmiyor.
Lab: Keycloak, Bir Realm ve İki Client
Keycloak'u geliştirme modunda, kendi port aralığımda çalıştırıyorum. Admin parolası çalışma klasöründe üretilip dosyada duruyor; yazıya girmiyor:
$ openssl rand -hex 16 > .kc-admin
$ docker run -d --name authwork2-keycloak -p 19200:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD="$(cat .kc-admin)" \
quay.io/keycloak/keycloak:26.8.2 start-dev
$ sh kc-setup.sh
realm ready
kc-setup.sh, Keycloak'un kendi CLI'ı (Command Line Interface) kcadm.sh ile bookstore realm'ini, rolleri, books.read/orders.read/orders.write scope'larını, test kullanıcılarını ve client'ları kuruyor. Kullanıcı parolaları her kurulumda rastgele üretiliyor. Realm'in sslRequired ayarı lab'da none; Docker'ın port yönlendirmesi istekleri "dışarıdan" gösterdiği için aksi hâlde Keycloak HTTP (Hypertext Transfer Protocol) üzerinden token vermiyor. Production'da bu ayar en az external kalır. Bu yazıda iki client kullanıyoruz. Birincisi curl'le adım adım gideceğimiz public client:
# Adım adım curl akışı için public client
$K create clients -r bookstore -f - <<'EOF'
{ "clientId": "bookstore-spa", "enabled": true, "publicClient": true, "standardFlowEnabled": true,
"implicitFlowEnabled": false, "directAccessGrantsEnabled": false,
"redirectUris": ["http://localhost:19211/callback"], "webOrigins": [],
"attributes": { "pkce.code.challenge.method": "S256", "post.logout.redirect.uris": "http://localhost:19211/" },
"defaultClientScopes": ["basic", "profile", "email", "roles", "acr", "web-origins"],
"optionalClientScopes": ["books.read", "orders.read"] }
EOF
pkce.code.challenge.method değeri S256 olunca Keycloak bu client için PKCE'yi (Proof Key for Code Exchange) zorunlu kılıyor ve plain method'unu kabul etmiyor. İkinci client bookstore-web, ASP.NET Core uygulamamızın confidential client'ı; ona PAR (Pushed Authorization Requests) bölümünde geleceğiz.
Bir not: defaultClientScopes satırını ilk kurulumda yazmamıştım. JSON'da (JavaScript Object Notation) yalnız optionalClientScopes verince Keycloak client'ın varsayılan scope listesini boş bıraktı ve ID token'da email, name gibi claim'ler hiç çıkmadı. Client'ı REST (Representational State Transfer) ya da kcadm ile kuruyorsan scope listelerinin ikisini de açıkça yaz.
PKCE: Code'u Onu İsteyene Bağlamak
PKCE'nin fikri basit. Client her giriş denemesi için rastgele bir code_verifier üretir ve onu kendinde tutar. Authorization request'e verifier'ın SHA (Secure Hash Algorithm)-256 hash'ini, yani code_challenge'ı koyar. Authorization server code'u bu challenge'a bağlar. Token isteğinde client verifier'ın kendisini gönderir; sunucu hash'i hesaplar, tutmazsa code'u reddeder. Front channel'dan geçen code'u ele geçiren biri verifier'ı bilmediği için code işe yaramaz.

.NET 10'un dosya tabanlı uygulama desteğiyle verifier ve challenge üretmek proje bile gerektirmiyor:
using System.Buffers.Text;
using System.Security.Cryptography;
using System.Text;
// RFC 7636: verifier 43-128 karakter; 32 rastgele byte base64url ile 43 karakter eder
string verifier = args.Length > 0 ? args[0] : Base64Url.EncodeToString(RandomNumberGenerator.GetBytes(32));
string challenge = Base64Url.EncodeToString(SHA256.HashData(Encoding.ASCII.GetBytes(verifier)));
Console.WriteLine($"code_verifier = {verifier} ({verifier.Length} chars)");
Console.WriteLine($"code_challenge = {challenge}");
$ dotnet run pkce.cs -- wpXcnBjHpSy2Y_wbJWecTSaYfE5aFbrOTMRTPQdkhsE
code_verifier = wpXcnBjHpSy2Y_wbJWecTSaYfE5aFbrOTMRTPQdkhsE (43 chars)
code_challenge = hIYMuMJf_BsAv40uTKW-h24GvxU3idGrqcIcE1LlL8s
Verifier'ı curl akışındaki shell script'i openssl ile üretmişti; C# aynı challenge'ı buldu. Base64Url class'ı .NET 9'dan beri System.Buffers.Text'te; padding'siz ve URL'de (Uniform Resource Locator) güvenli karakterlerle çalışıyor, eskiden elle yazılan Replace('+', '-') zincirine gerek kalmadı. plain method'unda challenge verifier'ın kendisi olduğu için front channel'ı okuyan biri verifier'ı da görür; RFC (Request for Comments) 9700 bu yüzden S256 diyor.
Authorization Request, state ve nonce
Script'in ürettiği değerler ve tarayıcıya gidecek adres:
verifier=wpXcnBjHpSy2Y_wbJWecTSaYfE5aFbrOTMRTPQdkhsE
challenge=hIYMuMJf_BsAv40uTKW-h24GvxU3idGrqcIcE1LlL8s
state=29c258ef06e655831e56f8d752803639 nonce=9a8cc1a06f517632c4d9b6a04a251102
GET /realms/bookstore/protocol/openid-connect/auth?response_type=code&client_id=bookstore-spa
&redirect_uri=http%3A%2F%2Flocalhost%3A19211%2Fcallback&scope=openid%20orders.read
&state=29c258ef06e655831e56f8d752803639&nonce=9a8cc1a06f517632c4d9b6a04a251102
&code_challenge=hIYMuMJf_BsAv40uTKW-h24GvxU3idGrqcIcE1LlL8s&code_challenge_method=S256
Keycloak giriş sayfasını döndürüyor. Lab'daki kc-login.sh bu sayfanın formunu curl'le dolduruyor (yalnız kendi realm'imiz için, tarayıcının yaptığını taklit etmek üzere); Ada'nın parolası script'in okuduğu yerel dosyada. Giriş başarılı olunca Keycloak tarayıcıyı client'a geri gönderiyor:
redirect: http://localhost:19211/callback?state=29c258ef06e655831e56f8d752803639
&session_state=DLUaeOI0oISA5WeLwGoGXQTI&iss=http%3A%2F%2Flocalhost%3A19200%2Frealms%2Fbookstore
&code=42ad8543-bd67-150a-81da-8e043a1791ad.DLUaeOI0oISA5WeLwGoGXQTI.405a7e9e-e386-45cf-b7a3-23117ad26033
state birebir geri geldi. Onu doğrulamak client'ın işi ve tek amacı şu: bu cevap, benim başlattığım bir isteğin cevabı mı? Doğrulamayan bir client'a saldırgan kendi hesabıyla aldığı bir code'u gönderebilir ve kurban saldırganın hesabına giriş yapmış olur (login CSRF (Cross-Site Request Forgery), Cross-Site Request Forgery). PKCE de aynı saldırıyı engeller, çünkü saldırganın code'u kurbanın tarayıcısındaki verifier'la tutmaz; RFC 9700 bu yüzden PKCE kullanan client'ta state'i CSRF için zorunlu tutmuyor. Ama state hâlâ işe yarıyor: ASP.NET Core onu dönüş adresini ve giriş öncesi bağlamı taşımak için kullanıyor.
iss parametresi RFC 9207'den geliyor. Birden fazla authorization server'la konuşan bir client, cevabın hangisinden geldiğini buradan kontrol eder; buna mix-up saldırısı savunması deniyor.
Token Request: Code'u Değiştirmek
Client şimdi back channel'dan token endpoint'ine gidiyor. Public client olduğu için secret yok; elindeki tek kanıt verifier:
$ curl -s -X POST $B/token -d grant_type=authorization_code -d client_id=bookstore-spa -d code=$CODE \
-d redirect_uri=http://localhost:19211/callback -d code_verifier=$VERIFIER
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzUxMiIs...",
"token_type": "Bearer",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"not-before-policy": 0,
"session_state": "DLUaeOI0oISA5WeLwGoGXQTI",
"scope": "openid profile orders.read email"
}
ID token'ın payload'ında nonce var:
{
"exp": 1794115985,
"iat": 1794115685,
"auth_time": 1794115685,
"jti": "4431e029-a9ee-21a7-b5d7-d0eae933c0d8",
"iss": "http://localhost:19200/realms/bookstore",
"aud": "bookstore-spa",
"sub": "813c410a-8e19-4979-85aa-5cc46f3cb5ab",
"typ": "ID",
"azp": "bookstore-spa",
"nonce": "9a8cc1a06f517632c4d9b6a04a251102",
"sid": "DLUaeOI0oISA5WeLwGoGXQTI",
"at_hash": "atSVIv9odwMtZsW0NuIdBw",
"acr": "1",
"email_verified": true,
"name": "Ada Lovelace",
"preferred_username": "ada@bookstore.test",
"given_name": "Ada",
"family_name": "Lovelace",
"email": "ada@bookstore.test"
}
state authorization response'u isteğe bağlıyordu; nonce ise ID token'ı isteğe bağlıyor. Client, gönderdiği nonce'u saklar ve gelen ID token'daki değerle karşılaştırır. Başka bir akıştan çalınmış ya da tekrar oynatılan bir ID token bu kontrolden geçemez. Ayrıntısını bir sonraki yazıda ID token'ı doğrularken işleyeceğiz.
Şimdi Önce Tahmin Et'teki sorular. Aynı code'u ikinci kez kullanmak:
--- same code again
{"error":"invalid_grant","error_description":"Code not valid"}
Ret beklenen sonuçtu; asıl ilginci ikinci denemenin ilk denemeye ne yaptığı. RFC 6749, bir code ikinci kez kullanılırsa sunucunun o code'la verilmiş token'ları mümkünse iptal etmesini öneriyor. Keycloak bunu yapıyor:
$ sh reuse.sh
userinfo with first access token: 200
same code again: {"error":"invalid_grant","error_description":"Code not valid"}
userinfo with first access token: 401
WWW-Authenticate: Bearer realm="bookstore", error="invalid_token", error_description="user_session_not_found"
İlk değişimde alınan access token, code tekrar denendiği anda geçersiz oldu; Keycloak bütün user session'ı kapattı. Mantığı şu: code iki kez geldiyse biri onu çalmıştır ve hangisinin meşru olduğunu sunucu bilemez; en güvenli cevap ikisini de düşürmek.
Yeni bir girişte önce yanlış verifier'la denedim, sonra doğrusuyla:
--- wrong verifier
{"error":"invalid_grant","error_description":"PKCE verification failed: Code mismatch"}
--- right verifier after a failed attempt
{"error":"invalid_grant","error_description":"Code not valid"}
Keycloak başarısız bir PKCE denemesinden sonra code'u yakıyor; gerçek client da artık o code'la token alamıyor. Bu doğru davranış: code'u ele geçiren biri verifier'ı tahmin ederek deneme yapamaz, ilk yanlışta code ölür. Kullanıcı açısından sonucu "giriş bir kez daha istenir" olur, güvenlik açığı değil.
Redirect URI: Birebir Eşleşme
Kayıtlı adres http://localhost:19211/callback. Sonuna bir path parçası ekleyip istek atınca:
$ curl -s -o r.html -w "%{http_code} redirect=%{redirect_url}\n" \
"$B/auth?response_type=code&client_id=bookstore-spa&redirect_uri=http%3A%2F%2Flocalhost%3A19211%2Fcallback%2Fevil&scope=openid&state=s1&code_challenge=abc&code_challenge_method=S256"
400 redirect=
$ grep -o 'Invalid parameter[^<]*' r.html
Invalid parameter: redirect_uri
Dikkat: bu sefer cevap redirect değil, Keycloak'un kendi hata sayfası. Redirect URI'ye (Uniform Resource Identifier) güvenilmiyorsa authorization server hatayı bile oraya göndermez; aksi hâlde kendisi bir open redirect olurdu. Code'un çalınmasının en eski yolu buydu: https://*.bookstore.example/* gibi gevşek bir kayıt, bir alt alan adında ya da path'te saldırganın kontrol ettiği bir sayfaya code'u teslim eder. RFC 9700 bu yüzden birebir string karşılaştırması istiyor; tek istisna native uygulamaların loopback adreslerindeki port numarası.
PAR: Authorization Request'i Back Channel'a Taşımak
Yukarıdaki authorization request'in bütün parametreleri tarayıcının adres çubuğundaydı: scope, redirect URI, challenge. Kimse onları imzalamıyor; kullanıcı ya da tarayıcıdaki bir eklenti değiştirebilir, log'lara düşer, uzun isteklerde URL sınırına takılır. PAR (Pushed Authorization Requests, RFC 9126) bunu tersine çeviriyor: client parametreleri önce back channel'dan, kendini doğrulayarak gönderir, karşılığında kısa ömürlü bir request_uri alır ve tarayıcıya yalnız onu verir.
$ curl -s -X POST $B/ext/par/request -u bookstore-web:$WEB_SECRET \
-d response_type=code -d client_id=bookstore-web -d redirect_uri=http://localhost:19212/signin-oidc \
-d scope=openid -d state=s1 -d nonce=n1 \
-d code_challenge=hIYMuMJf_BsAv40uTKW-h24GvxU3idGrqcIcE1LlL8s -d code_challenge_method=S256
{"request_uri":"urn:ietf:params:oauth:request_uri:a425a93c-ab4c-fff3-20e5-452e5f4dfb6e","expires_in":60}
Authorization server parametreleri client'ı doğruladıktan sonra aldığı için redirect URI'nin ve scope'un gerçekten o client'tan geldiğinden emin. Keycloak'ta bookstore-web için require.pushed.authorization.requests açık; PAR'sız klasik bir istek atınca:
302 redirect=http://localhost:19212/signin-oidc?error=invalid_request&error_description=Pushed+Authorization+Request+is+only+allowed.&state=s1&iss=http%3A%2F%2Flocalhost%3A19200%2Frealms%2Fbookstore
ASP.NET Core'un OIDC handler'ı PAR'ı .NET 9'dan beri destekliyor ve varsayılan davranış PushedAuthorizationBehavior.UseIfAvailable: discovery dokümanında pushed_authorization_request_endpoint varsa PAR kullanılıyor. Ben Require seçtim; sunucu PAR desteğini ilan etmezse handler giriş başlatmak yerine hata veriyor:
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = builder.Configuration["Oidc:Authority"];
options.RequireHttpsMetadata = false; // yalnız yerel lab: Keycloak HTTP üzerinde
options.ClientId = builder.Configuration["Oidc:ClientId"];
options.ClientSecret = builder.Configuration["Oidc:ClientSecret"];
options.ResponseType = OpenIdConnectResponseType.Code;
options.UsePkce = true; // varsayılan zaten true
options.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Require;
options.Scope.Add("orders.read");
options.MapInboundClaims = false;
options.SaveTokens = true;
options.Events.OnRemoteFailure = context =>
{
// 500 yerine kullanıcıya anlaşılır bir sayfa; ayrıntı log'da kalır
context.Response.Redirect("/login-failed");
context.HandleResponse();
return Task.CompletedTask;
};
});
Client secret configuration'dan, lab'da bir environment variable'dan geliyor. Korumalı /me endpoint'ine giriş yapmadan gidince handler'ın ürettiği cevap:
$ curl -s -c $J -b $J -o /dev/null -D - http://localhost:19212/me
HTTP/1.1 302 Found
Location: http://localhost:19200/realms/bookstore/protocol/openid-connect/auth?client_id=bookstore-web&request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3A80017a6c-7036-1336-49cb-c5a39fb61d1e&x-client-SKU=ID_NET10_0&x-client-ver=8.19.2.0
Set-Cookie: .AspNetCore.OpenIdConnect.Nonce.CfDJ8EINq_wm4TFCv7guzy6SHCeuOtSoMRF...
Set-Cookie: .AspNetCore.Correlation.t48krqAVVqWBPBzW7xjhPVMNE70F8v7XQsO9ZKAeMvg=N; expires=Sun, 08 Nov 2026 05:43:55 GMT; path=/signin-oidc; secure; samesite=none; httponly
Tarayıcıya giden adreste ne scope ne redirect URI ne challenge var; yalnız client_id ve request_uri. Handler PKCE verifier'ını ve nonce'u kendisi üretti, nonce'u bir cookie'ye, verifier'ı da Data Protection ile şifrelenen state'in içine koydu. Correlation cookie'si ise state'in bu tarayıcıda başlatılan bir girişe ait olduğunu kanıtlamak için.
ASP.NET Core Handler'ını Uçtan Uca Okumak
Giriş sonrası Keycloak, handler'ın varsayılanı olan response_mode=form_post ile cevap veriyor: code bir redirect URL'sinde değil, kendini otomatik gönderen bir HTML (HyperText Markup Language) formunun içinde. Böylece code adres çubuğuna, tarayıcı geçmişine ve Referer header'ına hiç düşmüyor. Formu curl'le gönderiyorum:
POST http://localhost:19212/signin-oidc
code=5d3485ed-f751-cd3c-adf9-ae4473eabebd.Dz09ULJGGeLDc-Gt5kPEM2kl.03d6f275-1a6e-4be6-abde...
iss=http%3A%2F%2Flocalhost%3A19200%2Frealms%2Fbookstore
state=CfDJ8EINq_wm4TFCv7guzy6SHCf432YIkpqMLbTf10VRbzuqu724eGtR-EmbZNFTEXUqKv9e9wNb1C2a-Ctp...
session_state=Dz09ULJGGeLDc-Gt5kPEM2kl
HTTP/1.1 302 Found
Location: /me
Set-Cookie: .AspNetCore.Correlation.YM0VoegJZX_OCnBas2tRuBlI63mQQS_bgb0PgMuGiQw=; expires=Thu, 01 Jan 1970 00:...
Set-Cookie: .AspNetCore.OpenIdConnect.Nonce.CfDJ8EINq_wm4TFCv7guzy6SHCcD9xz9BLLVzzYxfsn0inpO-VSQXKSYwhGBI-hdBF...
Set-Cookie: .AspNetCore.Cookies=chunks-2; path=/; samesite=lax; httponly
Set-Cookie: .AspNetCore.CookiesC1=CfDJ8EINq_wm4TFCv7guzy6SHCeVsDpecHFlSPnOXIh6ALmixyqA1X4ydQWU8e2cPFrIKoSA4MN5...
Set-Cookie: .AspNetCore.CookiesC2=loF0LRcSi2KayQojTy8FVbIatzlUp5l2_KMbMVRhuim6yn4gv8g2DzyCOuBljBUEzS8DImR1CdIh...
$ curl -s -b $J http://localhost:19212/me
{"name":"Ada Lovelace","email":"ada@bookstore.test","sub":"813c410a-8e19-4979-85aa-5cc46f3cb5ab"}
Handler correlation ve nonce cookie'lerini sildi, code'u back channel'dan token'la değiştirdi, ID token'ı ve nonce'u doğruladı ve cookie authentication ile oturum açtı. SaveTokens = true üç token'ı da cookie'ye koyduğu için cookie iki parçaya bölündü; 02. yazıda gördüğümüz chunking. Debug log'u bu adımları sırayla gösteriyor:
dbug: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[13]
Updating configuration
dbug: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[19]
Redeeming code for tokens.
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[10]
AuthenticationScheme: Cookies signed in.
Şimdi state'in bir karakterini değiştirip aynı formu gönderiyorum. OnRemoteFailure'ı eklemeden önceki çıktı bu:
--- tampered state
500
fail: Microsoft.AspNetCore.Server.Kestrel[13]
Microsoft.AspNetCore.Authentication.AuthenticationFailureException: An error was encountered while handling the remote login.
---> Microsoft.AspNetCore.Authentication.AuthenticationFailureException: Unable to unprotect the message.State.
State Data Protection ile şifrelenip imzalandığı için tek karakter değişince çözülemiyor. Aynı (geçerli) formu ikinci kez göndermek, yani code'u tekrar oynatmak da başarısız: correlation cookie'si ilk başarılı girişte silindi.
warn: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[15]
'.AspNetCore.Correlation.CObPNGzsOQveOqZMC_MHFadfBLtAwk40N_wJtmNinAU' cookie not found.
Error from RemoteAuthentication: Correlation failed..
replay: 302 http://localhost:19212/login-failed
OnRemoteFailure eklendikten sonra kullanıcı 500 sayfası yerine /login-failed'a gidiyor; ayrıntı log'da kalıyor. Production'da "Correlation failed" hatasını görürsen ilk bakacağın yer cookie'ler: correlation cookie SameSite=None; Secure ile yazılıyor, HTTPS (HTTP Secure) olmayan bir ortamda ya da TLS'i (Transport Layer Security) sonlandıran bir proxy'nin arkasında forwarded header'lar eksikse tarayıcı onu geri göndermez.
Üretimde Dikkat
Birden fazla instance'ın arkasında çalışan bir uygulamada state, nonce ve correlation cookie'leri Data Protection ile korunuyor; key ring'i instance'lar arasında paylaşmazsan giriş bir instance'ta başlayıp diğerinde bittiğinde "Unable to unprotect the message.State" görürsün. Load balancer'da sticky session değil, paylaşılan key ring çözüm.
PAR authorization server'a bir istek daha ekliyor; girişin başında bir back channel gidiş dönüşü. Giriş saniyede binlerce olmadığı sürece bunu ölçmeye değmez, ama authorization server'ın erişilemediği durumda Require ile UseIfAvailable farklı davranır: ilki girişi tamamen durdurur. Bunu bilerek seç. RequireHttpsMetadata = false yalnız yerel lab için; production'da discovery ve JWKS (JSON Web Key Set) HTTPS üzerinden gelmezse handler'ın imza doğrulaması anlamını yitirir.
Redirect URI'leri ortam başına ayrı kaydet: geliştirme, test ve production için ayrı client ya da en azından ayrı birebir URI listesi. "Kolay olsun" diye production client'ına http://localhost eklemek, herhangi bir makinede çalışan bir uygulamanın production code'unu almasına izin vermek demek.
Sık Düşülen Tuzaklar
Birincisi, wildcard redirect URI. Keycloak * desteğini hâlâ sunuyor; kullanmak RFC 9700'e aykırı. İkincisi, kendi yazdığın bir callback'te state'i üretip doğrulamamak ya da sabit bir değer kullanmak. Üçüncüsü, PKCE'yi "sadece mobil ve SPA (Single-Page Application) için" sanmak: RFC 9700 confidential client'lara da öneriyor, çünkü secret code injection saldırısını engellemez; code'u kimin başlattığını yalnız verifier kanıtlar. ASP.NET Core'da UsePkce'yi kapatmak için tek bir iyi neden yok.
Dördüncüsü, authorization request'e response_mode=query yazıp code'u adres çubuğuna taşımak; form_post varsayılanını korumak için bir sebebin yoksa dokunma. Beşincisi, "Correlation failed" hatasını susturmak için correlation cookie'sinin SameSite değerini düşürmek ya da kontrolü atlamaya çalışmak. Altıncısı, plain PKCE method'unu kabul eden bir sunucu ayarı. Keycloak'un discovery dokümanı code_challenge_methods_supported'da plain'i de ilan ediyor. Lab'da ayarsız bir geçici client'a plain challenge'la istek attım, giriş sayfası açıldı (200); bookstore-spa'ya aynı isteği atınca code challenge method is not matching the configured one hatası döndü. Client'a S256'yı açıkça yaz.
Küçük Bir Deneme
★☆☆ flow.sh'ı çalıştır ve aldığın code'u 60 saniye bekleyip kullan. Keycloak'un code ömrünü (realm ayarlarında "Client login timeout") bulup cevabı onunla karşılaştır.
★★☆ bookstore-web client'ında require.pushed.authorization.requests'i kapat ve ASP.NET Core'da PushedAuthorizationBehavior.Disable seç. Challenge cevabındaki Location header'ını PAR'lı hâliyle karşılaştır; hangi parametrelerin adres çubuğuna döndüğünü listele.
★★★ İki ayrı realm kur (bookstore ve bookstore-partner) ve ASP.NET Core'a ikisi için iki OIDC scheme ekle. Bir realm'in authorization response'unu diğerinin callback'ine gönderip handler'ın iss ve state üzerinden bunu nasıl yakaladığını log'da göster.
Code'u token'a çevirmeyi ve bu sırada araya girebilecek her şeyi gördük. Bir sonraki yazıda OIDC'nin kendisine geçiyoruz: ID token'ın access token'dan farkı, UserInfo ve discovery, AddOpenIdConnect'in claim mapping'i neden değiştirdiği ve logout'un üç türü, yani session'ı gerçekten bitirmek.