Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

Asp.NET Core Uygulamalarında Keycloak İle Temel Authentication İşlemleri #6

Merhaba,


Bu içeriğimizde Keycloak’da en temel seviyede authentication işlemlerini ele alacak ve üretilmiş bir token’ın hem manuel hem de .AddAuthentication() middleware’i ile profesyonel doğrulamalarının nasıl yapılacağını değerlendiriyor olacağız. Tabi tüm bu çalışma boyunca sürece dair gerekli detayları masaya yatırmış ve Keycloak’da ki ‘Public Key‘ ve ‘JWTKS‘ kavramlarının mahiyetini de net bir şekilde anlamış olacağız. O halde buyurun başlayalım…

Keycloak’da Üretilmiş Token’ı Asp.NET Core’da Doğrularken…

Keycloak’da üretilmiş bir token’ı Asp.NET Core’da doğrularken başta kimin hangi sorumluluklara sahip olduğunun farkını çizmekte fayda görmekteyim. Şöyle ki; süreçte Keycloak’un fiili görevi yalnızca token (JWT) üretimidir. Bu token’ın doğrulanma sorumluluğu ise sade ve sadece Asp.NET Core uygulamasındadır.

Asp.NET Core Uygulamalarında Keycloak İle Temel Authentication İşlemleri #6Haliyle bu doğrulama sürecinde Asp.NET Core;

  • Bu token gerçekten ilgili Keycloak tarafından mı üretildi?
  • İmzası doğru mu?
  • Süresi dolmuş mu?
  • Audience / issuer bilgileri doğru mu?

şeklinde sorular sorarak gerekli kontrolleri sağlamakla mükelleftir. Bunun nedeni, dünyadaki sayısız geçerli/geçersiz token arasından yalnızca tasarlanan sisteme ait, güvenilen ya da kabul edilen otorite tarafından üretilmiş token’ları ayırt edebilmektir. Ne de olsa, sisteme gelen her token JWT yapısı gereği geçerli görünebilir, ancak bu token’ın gerçekten beklenen issuer ve audience değerleriyle oluşturulup oluşturulmadığı, ve geçerli bir imzaya ve süreye sahip olup olmadığı tasarladığımız sistemimiz için oldukça önem arz edecektir.

Böylece her -selamün aleyküm- diyen token’ı kabul etmeyecek, yalnızca kendi sistemimize ait, güvendiğimiz identity provider tarafından üretilmiş token’ları ayıklayıp kabul etmiş olacağız.

Bunu gerçek hayattaki tiyatro biletleriyle aynı mantık olarak düşünebiliriz. Nasıl ki bir tiyatro oyununa girilebilmesi için dünyada üretilmiş herhangi bir bilete değil, sadece o oyuna ait, ve o oyunun organizatörü tarafından basılmış bir bilete ihtiyaç varsa evet, aynı mantık birebir token için de geçerlidir. Nasıl ki, bir tiyatro biletinde hangi oyuna ait olduğu, hangi organizatör tarafından basıldığı, geçerlilik süresinin (ki oyun tarihi geçtiyse yine geçersizdir) ne kadar olduğu ve biletin sahte mi gerçek mi olduğu bilgileri mevcutsa, token’da da bu tarz sisteme özel doğrulayıcı parametreler mevzu bahistir diyebiliriz.

Authentication Sürecinde Public Key Nedir? Ne İşe Yaramaktadır?

Keycloak’taki Public Key, Keycloak’un ürettiği token’ları imzalarken kullandığı RSA veya ECDSA gibi asimetrik şifreleme anahtar çiftinin public kısmıdır.

Keycloak; token üretirken private key ile imzalar, bu token’ı doğrulayan sistemler ise public key ile bu imzayı kontrole tabi tutar.

Keycloak, kullanıcıları doğruladıktan sonra uygulamalara (client’lara) genellikle JWT formatında token’ı verirken, bu token’ı dijital olarak imzalamaktadır (signed) Bu imzalama private key tarafından Keycloak sunucusunda gerçekleştirilir. Uygulamalar ise public key ile -bu token gerçekten ilgili Keycloak tarafından mı oluşturuldu?-, -hiç değiştirilmedi mi?- şeklinde soruların cevabını arar.

Neden public key’e ihtiyaç vardır?
Çünkü modern mimarilerde (özellikle microservice, API Gateway, SPA + backend kombinasyonları) token doğrulaması için merkezi olan authorization server’a (bizdeki mevzu bahis Keycloak) her istekte danışmak pek doğru değildir. Düşünsenize… Bir istek geliyor ve authorization server’a gidip -bu isteği yapan token senden mi?- diye soruyoruz. Sonra bir başka istekte yine soruyoruz. Sonra bir başkasında yine… Böyle bir durumda authorization server sanki amacından sapıp doğrulama sürecine eşlik eden farklı bir misyona sahipmiş gibi olacaktır. Halbuki token doğrulamasının en doğrusu, yerel olarak doğrulamadır. Yani ilgili uygulamada/API’de/client’ta doğrulama yapmaktır. İşte bunun için public key kullanılmaktadır. Public key bir kez alınmakta ve servisler tarafından bu anahtar değer kullanılarak kendi başlarına token doğrulaması gerçekleştirilmektedir.

Eğer public key olmasaydı, her istekte Keycloak’a -bu token geçerli mi?- diye sormak zorunda kalacak, böylece ağ trafiğini artıracaktık. Halbuki public key sayesinde daha hızlı doğrulama süreci sağlayabilmekte, ağ trafiğini azaltabilmekte ve performanstan ciddi kazanç elde edilebilmektedir. Ayrıca Keycloak sunucusu gitse bile mevcut token’ların expire süresi boyunca çalışmaya devam etmesi bile sağlanabilmektedir.

Public key ile sağlanan bu modele Stateless Authenticaiton denmektedir.

Peki public key nereden alınır?
Public key’i alabilmek için Keycloak’un sunduğu aşağıdaki endpoint’inden istifade edebiliriz:

🔗 /realms/{realm}/protocol/openid-connect/certs

Asp.NET Core Uygulamalarında Keycloak İle Temel Authentication İşlemleri #6Bu endpoint’e yapılan GET isteği neticesinde public key bizlere yandaki görselde olduğu gibi JWKS (JSON Web Key Set) formatında dönecektir.

Dikkat ederseniz, biri RSA-OAEP bir diğeri ise RS256 algoritmalarında oluşturulmuş farklı amaçlara sahip iki adet RSA anahtarı görüyoruz.

Bunlardan;

RSA-OAEP, Keycloak’un token’ları şifrelemek için kullandığı anahtarın ta kendisidir. Ama dikkat, bu değer public key değil, private key’dir. Çünkü mevzu bahis şifrelemeyse o taktirde bu şifreyi çözebilmek için bizlerin kesinlikle private key’e ihtiyacı olacaktır. O yüzden
RSA-OAEP, pek önerilmemekte ya da daha doğru ifadeyle çoğu projede tercih edilmemektedir.

RS256 ise token’ların imzasını doğrulamak için kullanılan public key’in ta kendisidir.

Bu anahtar değerin nerede ve nasıl kullanılacağına içeriğimizin devamındaki satırlarda değiniyor olacağız. Şimdi bizler yavaştan konuyu pratik zemine kaydıralım ve authentication yapılanmasını manuel olarak nasıl gerçekleştirebileceğimize odaklanalım.

Manuel Olarak Token’ı Validate Edelim…

İlk olarak yapmamız gereken, manuel doğrulamayı gerçekleştirecek olan sınıfı Microsoft.AspNetCore.Authentication.JwtBearer kütüphanesi eşliğinde aşağıdaki gibi tasarlamaktır:

    public class ManualJwtValidator(IHttpClientFactory _httpClientFactory)
    {
        public async Task<ClaimsPrincipal> ValidateAsync(string token, string realm = "master")
        {
            var httpClient = _httpClientFactory.CreateClient("keycloak");
            var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();

            var tokenValidationParameters = new TokenValidationParameters
            {
                // Token'ı kimin oluşturduğu kontrol edilecek.
                ValidateIssuer = true,
                // Beklenen oluşturucu adresi (Keycloak realm URL'i)
                ValidIssuer = $"{httpClient.BaseAddress.AbsoluteUri}realms/{realm}",
                // Token'ın kime hitap ettiği kontrol edilecek.
                ValidateAudience = true,
                // Beklenen hedef kitle.
                ValidAudience = "account",
                // Token'ın geçerlilik süresi kontrol edilecek.
                ValidateLifetime = true,
                // Token imzasının doğruluğunu kontrol edilecek.
                ValidateIssuerSigningKey = true,
                // Token imzasını doğrulamak için public key'i alınmakta ve doğrulanmaktadır.
                IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) =>
                {
                    // Keycloak'tan JWKS endpoint'inden public key getirilmektedir.
                    var publicKey = JWKSService.GetKeysAsync(httpClient, "master").Result;
                    return publicKey;
                },
                // JWT doğrulama işlemi, claim'leri otomatik olarak 'Identity.Name' alanına eşlememektedir! Bu alanın hangi claim ile dolacağı bu property üzerinden belirlenmelidir.
                NameClaimType = JwtRegisteredClaimNames.PreferredUsername
            };

            var tokenValidationResult = await jwtSecurityTokenHandler.ValidateTokenAsync(token, tokenValidationParameters);
            return new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity);
        }
    }

Burada, klasik Asp.NET Core çalışmalarından aşina olunduğu üzere bildiğiniz JWT doğrulama parametreleri yapılandırılmaktadır. Gerekli açıklamaları kod üzerinde yaptığımdan dolayı pek detaya girmeye lüzum görmesem de 26. satıra dikkatinizi çekmek istiyorum. Bu satırda, içeriği aşağıdaki gibi tarafımızca oluşturulmuş JWKSService.GetKeysAsync metodu kullanılmaktadır:

    public class JWKSService
    {
        public static async Task<IList<SecurityKey>> GetKeysAsync(HttpClient httpClient, string realm = "master")
        {
            // Keycloak'un JWKS endpoint'inden public key'leri alıyoruz.
            var certsResponse = await httpClient.GetStringAsync($"/realms/{realm}/protocol/openid-connect/certs");
            // JSON string'i JsonWebKeySet nesnesine dönüştürüyoruz. Bu, içerisinde token'ı doğrulamak için kullanılacak key'leri barındıran bir koleksiyondur.
            var certsJWKS = new JsonWebKeySet(certsResponse);
            // Key set'inden signed key'leri ayıklayıp, gönderiyoruz.
            var keys = certsJWKS.GetSigningKeys();
            return keys;
        }
    }

Bu metotta, token’ı doğrulayabilmek için JWKS endpoint’inden elde edilen public key listesi elde edilmekte ve bu liste IssuerSigningKeyResolver property’sine sunulmaktadır. Böylece doğrulama için gerekli olan tüm konfigürasyonel değerler yapılandırma sürecinde uygun yerlere set edilmektedir. Artık, bu aşamadan sonra geriye token’ı aşağıdaki gibi doğrulamak kalmaktadır:

app.MapGet("/secure/{realm}", async (ManualJwtValidator manualJwtValidator, [FromHeader(Name = "Authorization")] string authorization, string realm = "master") =>
{
    if (!authorization.StartsWith("Bearer "))
        return Results.Unauthorized();

    var token = authorization.Substring("Bearer ".Length);
    var principal = await manualJwtValidator.ValidateAsync(token, realm);

    return TypedResults.Ok(new
    {
        GivenName = principal.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.GivenName)?.Value,
        UserName = principal.Identity?.Name,
        Claims = principal.Claims.Select(c => new { c.Type, c.Value })
    });
});

Asp.NET Core Uygulamalarında Keycloak İle Temel Authentication İşlemleri #6Evet… Bu çalışma neticesinde ilgili endpoint’e, Authorization header key’ine Bearer {token} şeklinde değer yerleştirilmiş bir request atıldığı taktirde doğrulama gerçekleştirilecek ve netice yandaki görseldeki gibi elde edilecektir.

Manuel doğrulamadan bu şekilde yetinilebilir. Ancak bu çalışmayı aşağıdaki gibi custom middleware ile bir seviye daha profesyonelliğe taşıyarak, endpoint içerisinde token ayıklamasıyla vs. uğraşılmayacak şekilde yarı manuel hale getirebiliriz.

Yarı Manuel Olarak Token’ı Validate Edelim…

    public class JwtAuthenticationMiddleware(IHttpClientFactory _httpClientFactory, ManualJwtValidator _manualJwtValidator) : IMiddleware
    {
        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            var authorization = context.Request.Headers[HeaderNames.Authorization].ToString();
            if (authorization is not null)
            {
                var token = authorization.Substring("Bearer ".Length);
                var principal = await _manualJwtValidator.ValidateAsync(token);

                context.User = principal;
            }

            await next(context);
        }
    }

Görüldüğü üzere, yukarıdaki gibi custom bir middleware sayesinde manuel authentication’ı uygulamadaki tüm request’lere uygulayacak şekilde genişletmiş bulunuyoruz. Tabi bu middleware’in işlevsel olabilmesi için mimariye aşağıdaki gibi eklenmesi gerekmektedir.

app.UseMiddleware<JwtAuthenticationMiddleware>();

Artık bu aşamadan sonra gerisi testtir 🙂 Aşağıdaki endpoint’e yine aynı authorization header key’i eşliğinde istekte bulunabilir ve kimlik doğrulama durumunu kontrol edebilirsiniz:

app.MapGet("/", (HttpContext httpContext) => httpContext.User.Identity?.IsAuthenticated);

Tabi bu şekilde manuel doğrulamanın getirdiği sıkıntılar yok değil! Şimdi gelin bu sıkıntıları masaya yatırıp farkındalık oluşturalım.

Manuel Doğrulamanın Getirdiği Sıkıntılar

Manuel doğrulama sürecinde, kullanıcının identity’sini ve sahip olduğu tüm bilgileri (claim’leri) temsil eden ClaimsPrincipal nesnesi üretiliyor, üretilmesine ama bu HttpContext.User‘a tam olarak set edilmiyor! Dolayısıyla bu durumda [Authorize] doğru düzgün çalışma sergileyemiyor! Neden mi? Çünkü, Asp.NET Core authentication pipeline’ı tam çalışmıyor da ondan 😉 Yani Asp.NET Core, bu isteğin hangi yöntemle doğrulandığını bilmiyor, bu doğrulamayı kendi mekanizmasıyla yapmış saymıyor ve tüm bu süreci kendi authentication işleyişinin bir parçası olarak kabul etmiyor!

Dolayısıyla netice itibariyle bizler doğrulamayı her ne kadar başarılı yapmış olursak olalım framework bu isteğin authenticated olduğunu bilmemektedir! Burada bizim, token’ı doğrulamanın akabinde vaziyeti, Asp.NET Core’un authentication sistemine -bu request, şu AuthenticationScheme ile doğrulandı- şeklinde bildirmemiz gerekmektedir. Çünkü [Authorize] attribute’u, ilgili request’in, registered edilmiş bir authentication scheme tarafından doğrulanmış olup olmadığına bakmaktadır… Ee bizler manuel doğrulamada, doğrulamayı başarılı gerçekleştirmiş olsak da herhangi bir authentication scheme ile bir bildirimde bulunmadığımız için [Authorize] açısından durum olumsuz değerlendirilecektir.

AuthenticationScheme, bir request geldiğinde hangi mekanizma ile kimlik doğrulaması yapılacağının cevabını verir… JWT mi, cookie mi, başka bir mekanizma mı…

AuthenticationScheme bu kadar şart mı? diye sorarsanız, evet şart… Çünkü Asp.NET Core’da aynı anda birden fazla authentication yöntemi desteklenebilmektedir ve o anda hangi yöntemle doğrulama yapıldığı sistem tarafından bilinmediği sürece mimarinin davranışı şekillenmemektedir.

Tüm bunların dışında, manuel doğrulama sürecinde olası red, yetkisizlik veya doğrulanamama durumlarında sistem tarafından otomatik verilecek cevap türleri olan Challenge ve Forbidden tasarlanmadığı için sistem kimin neden reddedildiğini ayırt edememekte ve client’a doğru ve standart bir bildirimde bulunamamaktadır.

Toparlarsak eğer; manuel token doğrulama, token’ın geçerli olup olmadığını anlamak için yeterlidir lakin Asp.NET Core’un [Authorize], role, policy, 401/403 ayrımı ve authentication lifecycle’ını düzgün çalıştırabilmesi için de mutlaka bir AuthenticationScheme üzerinden sisteme entegre edilmesi gerekmektedir. İşte bu nedenle kimlik doğrulama sürecinde en doğru, temiz ve Keycloak için de oldukça uyumlu olan aşağıdaki profesyonel yolu uygulamak gerekmektedir.

Profesyonel Yol – .AddAuthentication().AddJwtBearer()

Profesyonel seviyede authentication yapılandırmasında bulunabilmek için Asp.NET Core mimarisine aşağıdaki gibi AddAuthentication ve AddJwtBearer yapılandırmalarının bulunulması yeterlidir:

// Authentication mekanizmasının JWT Bearer olacağını belirtiyoruz.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                // JWT Bearer token'larını işleyecek ve doğrulayacak handler'ı yapılandırıyoruz.
                .AddJwtBearer(options =>
                {
                    // Token'ı doğrulayacak otorite (Keycloak server) adresini belirtiyoruz. Public key, ilgili kütüphane tarafından bu otoritenin certs adresinden otomatik alınacaktır.
                    options.Authority = "http://127.0.0.1:8080/realms/master";

                    options.Audience = "account";

                    options.RequireHttpsMetadata = false;

                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        NameClaimType = JwtRegisteredClaimNames.PreferredUsername
                    };
                });

Burada dikkatinizi direkt, issuer tanımlaması ile birlikte public key çalışmasının olmayışına çekmek isterim. Evet, profesyonel yol olarak isimlendirdiğimiz bu yaklaşımda issuer ve public key tanımlamalarını manuel yapmamıza gerek bulunmamaktadır. Bunun nedeni AddJwtBearer servisinde tanımladığımız Authority yapılandırmasıdır. Bu yapılandırma sayesinde Asp.NET Core, .well-known endpoint’ine otomatik bir şekilde istekte bulunarak hem issuer hem de public key bilgilerine erişmekte ve yapılandırmaya kendi dahil etmektedir.

Yapılandırmayı bu şekilde tasarladıktan sonra mimarinin pipeline’ına aşağıdaki middleware’in eklenmesi gerekmektedir:

app.UseAuthentication();

Böylece gelen request’teki token’ı okuyup, yaptığımız yapılandırma kuralları çerçevesinde kullanıcı doğrulanmaya çalışılacaktır.

Evet, böylece Keycloak’da doğrulamayı daha profesyonel ve olması gereken ideal haliyle gerçekleştirmiş bulunmaktayız. Bu yöntemle; Keycloak JWKS bilgileri otomatik edinilmekte, key rotation desteklenmekte, token cache edilebilmekte, 401 / 403 yönlendirmeleri otomatik olarak yönetilebilmekte ve tüm bu yapılandırmalar Asp.NET Core security pipeline’ına standart bir şekilde entegre olarak gerçekleştirilmektedir 🙂

İşte bu kadar basit 🙂

Bundan sonraki içeriklerimizde artık bu bilgi zeminini kullanarak ilerleyecek ve akışları, bunların bilindiği varsayımla inceliyor olacağız.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Örnek çalışmaya aşağıdaki GitHub reposundan erişebilirsiniz.
https://github.com/gncyyldz/Keycloak.Examples

Bu repository, ilgili projenin kaynak kodlarını ve mimari yapısını içermektedir. Detaylar için GitHub üzerinden inceleyebilirsiniz.


GitHub’da Görüntüle →

Bunlar da hoşunuza gidebilir...

1 Cevap

  1. emre dedi ki:

    Hocam emeğinize sağlık çok faydalı bir seri oldu.
    Bir sorum olacak;

    Birbirleri ile haberleşen API’lar arasında token taşımasını keycloack kendisi mi hallediyor.
    Örn: X clientı içinde A API’na istek atacak ve A API’nın clientid-secret değerini bildiği için token alıp istek attı. A API kendi içinde B API’nı kullanması gerekirse ilk token buraya taşınacak mı, taşınmalı mı. Bir tokenın izini yaptığı işlemleri takip etmek için gerekebilir.

    Yoksa A API kendi içinde B API’na istek atmak için B için de clientid-secret mı bilip token mı almalı?

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir