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

Asp.NET Core – Özelleştirilmiş Authentication ve Authorization Yapılanması

Merhaba,

Her web uygulamasında güvenlik açısından kullanıcı ve yetki yönetimi oldukça kritik arz eden konulardır. Dolayısıyla bu temel davranışlar, Asp.NET Core uygulamaları için de olmazsa olmazdır diyebiliriz. Bundan dolayı Asp.NET Core mimarisinde, kimlik doğrulama (authentication) ve yetkilendirme (authorization) süreçlerini yönetebilmek için oldukça güçlü ve esnek bir altyapı sunulmakta ve bu temel taşlar sayesinde role-based authorization’dan tutun claims veya policy authorization’a ve hatta Google, Facebook vs. gibi external authorization yapılanmalarına kadar birçok yetkilendirme yaklaşımı rahatlıkla sergilenebilmektedir. Tabi bu bahsi geçen hazır yapılanmalar genellikle kabul görmüş standartlar üzerinden davranışlarını şekillendirmekte olsa da esasında birçok durum ve senaryoya karşın en kılcal manevralara kadar yeterlilik göstermektedirler.

Bizler bu içeriğimizde var olan hazır mekanizmalardan ziyade, bu mekanizmaların standart’larının dışı durumlarda(senaryolarda) yani hazır yapıların erişemediği senaryotik kıvrımlarda nasıl çözümler getirebileceğimizi ve bu tarz süreçlerde özel davranışları geliştirebilmek için Asp.NET Core mimarisinde ne gibi nimetlerin bizlere sunulduğunu inceliyor olacağız.

Evet, bazen kimi senaryolarda Asp.NET Core’daki authentication ve authorization için varsayılan davranışlar standart dışı durumların söz konusu olmasından dolayı özelleştirilmiş gereksinimler gerektirebilmektedir. Örnek vermemiz gerekirse eğer, Asp.NET Core mimarisinde JwtBearer yapılanması JWT’leri belirli parametreler eşliğinde doğrulamak üzere tasarlanmış gayet basit ama efektif bir yapılanmadır. Ancak bazı durumlarda JWT yapısı ya da bu JWT’yi doğrulama mantığı mevcut ve genel kabul görmüş standartların dışında olabilmektedir.

Ne gibi standart dışı durumlar olabilir la hoca? diye sorarsanız eğer aşağıdaki olası durumları düşünebiliriz;

  • Kullanılan token, JWT değil farklı bir formatta değer olabilir.
  • Token’ın özelleştirilmiş bir şifreleme algoritmasıyla doğrulanması gerekiyor olabilir.
  • Token’da ki payload’ların özel bir işleme tabi tutulması gerekiyor olabilir.
  • vs.

İşte bunlar ve bunlara benzer normal şartların dışında özelleştirilmiş davranışlar gerektiren durumlarda bizlerin eldeki token’ı, sistemin sağladığı hazır yapılarla değil kendimizce bu davranışları karşılayacak özelleştirilmiş çalışmalarla kullanmamız ve işleme tabi tutmamız gerekecektir. Bu durumların ayırdını daha net bir şekilde ortaya koyabilmek için şöyle bir örneği değerlendirmekte fayda görmekteyim; kimi durumlarda token içerisinde yer alan claim’ler varsayılan doğrulama mantığının dışında işlenebilmektedirler. Buna en güzel örneği, keycloak ile doğrulama yapıldığı süreçlerde elde edilen token’daki rollerin manuel değerlendirilmesi sürecinden verebiliriz. Bu durumu tecrübe edenler(ki genellikle keycloak kullanılırken Keycloak.AuthServices.Authorization kütüphanesinin AddKeycloakWebApiAuthentication servsiyle bu yapılandırma hızlıca uygulanmaktadır. Ben bu yapılandırmayı manuel tecrübe edenlerden bahsediyorum) bilirler ki, keycloak’tan gelen token içerisindeki rollerin(yani claim’lerin) doğrulanabilmesi normal şartlara nazaran özelleştirilmiş bir değerlendirme/ayırt etme/işlem gerektirmektedir.

Ya da normal şartların dışında özelleştirilmiş bir authentication ve authorization davranışını şekillendirmemizi gerektirecek bir başka durum ise farklı sistemlerden gelen bilgilere göre yetkilendirmenin şekillendirilmesi gereken hibrit durumlardır diyebiliriz. Bu tarz hibrit gereksinimlerde token doğrulaması farklı, doğrulama neticesinde oturumun hala aktif olup olmadığı ise farklı veri kaynaklarından kontrol edilebilmektedir. İşte bu tarz davranışsal yapılanmalara çok nadir ihtiyaç hissedilse de, söz konusu oldukları takdirde özelleştirilmiş manuel yapılar inşa etmemiz kaçınılmaz olmaktadır.

Tüm bunların dışında, benimsediğiniz güvenlik ilkeleriniz JWT doğrulama yapılarını aşabilir, ek şifreleme katmanları kullanmak isteyebilir ya da token üzerinden ekstradan özelleştirilmiş işlemler yapmak isteyebilirsiniz. Veya token içerisindeki bazı bilgilerin işlenmeye gerek olmadığını düşünerek de performans iyileştirmesi gayesiyle doğrulama sürecini daha hafif hale getirmek istiyor da olabilirsiniz.

İşte burada bahsedilen senaryolarda ya da benzeri durumlarda kimlik doğrulama ve yetkilendirme süreçlerinin manuel bir şekilde optimize edilmesi gerekecektir. Şimdi gelin Asp.NET Core’da manuel bir şekilde authentication ve authorization işlemlerinin nasıl yapıldığını parça parça ele almaya başlayalım.

Asp.NET Core’da Manuel Bir Şekilde JWT Doğrulama

Her şeyden önce Asp.NET Core’da JWT doğrulama işlemini manuel olarak nasıl gerçekleştirebileceğimizi inceleyerek başlayalım. Tabi manuel davranışı net vurgulayabilmek için öncelikle bu güne kadar geleneksel olarak nasıl bir doğrulama süreci geçirdiğimizi mukayese amaçlı incelemekte fayda görmekteyim.

.
.
.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new()
        {
            ValidateAudience = true,
            ValidateIssuer = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["JWT:Issuer"],
            ValidAudience = builder.Configuration["JWT:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:SecurityKey"])),
            ClockSkew = TimeSpan.Zero
        };
    });

Yukarıdaki kod bloğunda, Asp.NET Core mimarisinde bir JWT’yi doğrulamak için hazır sunulan AddAuthentication servisini görmekteyiz. Bu servis, request’in header’ında Authorization key’ine karşılık gelen JWT’yi standart koşullara uygun şekilde doğrulamaya çalışmakta ve doğrulama neticesinde uygulamanın tüm gereksinimlerine uygun bir ClaimsPrincipal nesnesi oluşturarak yetkilendirmeyi gerçekleştirmektedir.

Bu işleme alternatif olarak aşağıdaki gibi kimlik doğrulama sürecini manuel bir şekilde gerçekleştirebilir ve sisteme dahil edip, kullanabiliriz;

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Net.Http.Headers;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;

namespace AspNetCoreCustomAuth.AuthenticationHandlers
{
    public class ManualAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        readonly IConfiguration _configuration;
        readonly IHttpContextAccessor _httpContextAccessor;

        public ManualAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            IConfiguration configuration,
            IHttpContextAccessor httpContextAccessor)
            : base(options, logger, encoder)
        {
            _configuration = configuration;
            _httpContextAccessor = httpContextAccessor;
        }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            string authenticationScheme = "Manual";

            JwtSecurityTokenHandler tokenHandler = new();
            var key = Encoding.ASCII.GetBytes(_configuration["JWT:SecurityKey"]);

            string token = _httpContextAccessor.HttpContext.Request.Headers[HeaderNames.Authorization];
            token = token?.Replace("Bearer ", "");

            ClaimsPrincipal? claimsPrincipal = null;
            try
            {
                claimsPrincipal = tokenHandler.ValidateToken(token, new TokenValidationParameters
                {
                    ValidateAudience = true,
                    ValidateIssuer = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = _configuration["JWT:Issuer"],
                    ValidAudience = _configuration["JWT:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:SecurityKey"])),
                    ClockSkew = TimeSpan.Zero
                }, out SecurityToken validatedToken);

                var ticket = new AuthenticationTicket(claimsPrincipal, authenticationScheme);
                return Task.FromResult(AuthenticateResult.Success(ticket));
            }
            catch
            {

            }

            return Task.FromResult(AuthenticateResult.Fail(string.Empty));
        }
    }
}

Görüldüğü üzere ManualAuthenticationHandler sınıfı ile kendimize özel bir authentication yapısı oluşturmuş bulunuyoruz. Burada token’ı manuel doğruladıktan sonra 54. satırda, AuthenticationTicket nesnesi ile kimliği doğrulanmış ilgili kullanıcının oturum bilgilerini temsil ediyor ve 55. satırda da bu nesne ile kimlik doğrulama işleminin başarılı olduğunu ifade eden AuthenticateResult.Success(ticket) sonucu bildiriyoruz. Eğer ki, kimlik doğrulama sürecinde bir başarısızlık mevzu bahisse o taktirde de AuthenticateResult.Fail(string.Empty) sonucunu döndürerek token’ın geçersiz olduğunu ifade ediyoruz.

Bu sınıfı sisteme dahil edebilmek için ‘Program.cs’ dosyasında aşağıdaki gibi bir yapılandırmada bulunmamız yeterli olacaktır;

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = "Manual";
    options.DefaultChallengeScheme = "Manual";
}).AddScheme<AuthenticationSchemeOptions, ManualAuthenticationHandler>("Manual", null);

İşte JWT’yi hazır yapılanmaların aksine, özelleştirilmiş davranışlarla manuel olarak nasıl doğrulayabileceğimizi mukayese ederek incelemiş olduk. Bu aşamadan sonra özel gereksinim yapılanması olan rol tabanlı yetkilendirmeyi incelemeye geçebiliriz.

Özel Gereksinim Yapılanması(Rol Tabanlı Yetkilendirme)

Peki, şimdi de özelleştirilmiş yetkilendirme kuralları uygulamak istediğimiz durumlarda kullanılabilecek yapıları inceleyelim. Normal şartlarda, Asp.NET Core yetkilendirme mekanizmaları rol tabanlı yetkilendirme davranışını rahatlıkla uygulayabilmemizi sağlamaktadır ancak kimi durumlar vardır ki özel şartlara bağlı yetkilendirme ihtiyaçları söz konusu olabilmektedir.

Bu gibi durumlarda manuel olarak yetkilendirme kuralları belirleyebilmek için IAuthorizationRequirement ve AuthorizationHandler yapılanmaları kullanılır. Şimdi bu yapılanmalar üzerinden farazi bir çalışma ile özel bir gereksinim oluşturalım. Misal olarak, token içerisinde ‘resource_access’ isimli alanda bulunan değerlere uygun rol tabanlı bir yetkilendirme çalışmasını sisteme entegre etmeyi hedeflediğimizi düşünürsek eğer bunun için öncelikle bu gereksinimi temsil edecek olan requirement sınıfını oluşturarak başlayabiliriz;

    public sealed record ResourceAccessRequirement(string Role) : IAuthorizationRequirement
    {

    }

Bu sınıf, Asp.NET Core’da yukarıdaki satırlarda ifade etmeye çalıştığım üzere özel yetkilendirme gereksinimini temsil etmektedir. Yani bir endpoint’e erişim gösterilmesi için hangi şartların yerine getirilmesi gerektiği bu sınıfla belirlenecektir. Ee hoca! bu güne kadar bu tarz bir sınıf oluşturmadan yetkilendirmemizi sağlıyorduk! dediğinizi duyar gibiyim. Evet, bu güne kadar token içerisindeki claim’leri direkt değersel olarak yorumlayıp ‘role’ alanı ‘admin’ ise bu endpoint’e erişim izni ver gibisinden çalışmalar sergiliyorduk. Ancak burada mevzu bahis olan senaryoda kontrolde bulunmak istediğimiz claim’de bir değerden ziyade alt değerlerin olduğunu varsayıyoruz ve işte bu durumda nasıl bir davranışın sergilenmesi gerektiğini konuşuyoruz. Yani anlayacağınız, token içerisindeki claim’lerde ‘resource_access’ alanı içerisindeki ‘x’ alanı ‘y’ ise bu endpoint’e erişim izni ver şeklinde bir özel çalışma yapılacaksa eğer bunu öncelikle bir requirement ile temsil etmemiz elzemdir ve ancak böyle bir davranışla bu özel ihtiyaç giderilebilmektedir.

Requirement’ı oluşturduktan sonra artık bu gereksinim karşılığında sergilenecek özel davranışı ifade edecek olan handler yapısını aşağıdaki gibi oluşturabiliriz;

    public sealed class ResourceAccessHandler : AuthorizationHandler<ResourceAccessRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ResourceAccessRequirement requirement)
        {
            var resourceAccessClaim = context.User.FindFirst("resource_access");

            if (resourceAccessClaim != null)
            {
                using var jsonDocument = JsonDocument.Parse(resourceAccessClaim.Value);
                var root = jsonDocument.RootElement;

                if (root.TryGetProperty("realm-management", out var realmManagement))
                    if (realmManagement.TryGetProperty("roles", out var rolesElement) && rolesElement.ValueKind == JsonValueKind.Array)
                    {
                        var roles = rolesElement.EnumerateArray().Select(role => role.GetString());
                        if (roles.Contains(requirement.Role))
                            context.Succeed(requirement);
                    }
            }

            return Task.CompletedTask;
        }
    }

Burada ise AuthorizationHandler sınıfı üzerinden Asp.NET Core’da belirli bir yetkilendirme gereksinimini karşılamak için özel bir mantık/davranış tanımlanmaktadır. Bu sınıf ile oluşturulan requirement’ın şartlarına/iş mantığına uygunluk durumu kontrol edilmekte, bir başka deyişle yetkilendirme kararı değerlendirilmektedir.

Yapılan çalışmaya göz atılırsa eğer önceki satırlarda bahsedildiği gibi token içerisindeki ‘resource_access’ alanı içerisinde bulunan alt alanların değerleri okunarak requirement’ta verilen ‘role’ değerinin karşılanıp, karşılanmadığı değerlendirilmekte ve duruma göre yetkilendirme kararı verilmektedir. Burada pekte konumuzla alakalı olmasa da genel kültür mahiyetinde JsonDocument‘tan da bahsetmekte fayda görmekteyim. Bu sınıf ile metinsel olarak elde edilen JSON veriler oldukça verimli bir şekilde okunabilmektedir. Haliyle yaptığımız çalışmaya tekrar göz atarsak, ‘resource_access’ alanı root eleman olarak elde edilmekte, eğer içerisinde ‘realm-management’ alanı varsa o alana girilmekte ve onun içerisinde de ‘roles’ alanı kontrol edilmektedir. Nihai olarak ‘roles’ alanı içerisinde requirement’a uygun bir değer var mı yok mu kontrol edilmekte ve böylece yetkilendirme verilmektedir. Demek ki bu uygulamada da rol tabanlı yetkilendirmenin gereksinimi bu şekildeymiş. Ee bu özel ve sipesifik olan gereksinime karşın da yapılacak davranış ancak bu şekilde olmak mecburiyetindedir 😉

Özetlersek eğer AuthorizationHandler sınıfında; claim’leri ve diğer ilgili gereksinimleri kullanarak erişim izninin verilip verilmeyeceğine dair kararlar verilmekte ve belirli bir requirement’ı karşılayan kullanıcıları doğrulamak için özel bir mantık oluşturulabilmektedir.

Ayrıca oluşturmuş olduğumuz bu sınıf içerisindeki HandleRequirementAsync metodunun AuthorizationHandlerContext context parametresinden de bahsetmekte fayda görmekteyim. Bu paremetre; yetkilendirme sürecindeki kullanıcıya dair kimlik bilgilerini(claims), yetkilendirme gereksinimlerini(requirements) ve yetkilendirilecek kaynaklarını içermektedir. Yani tüm bilgilere bu parametrelerden erişilebilmektedir. Ayrıca ilgili context’i kullanarak kullanıcının erişim iznine sahip olup olmadığını da belirlemektedir. Bu paramerenin bizleri ilgilendiren önemli özellikleri şunlardır;

  • context.User
    Kullanıcının kimlik bilgilerini ve rollerini(claims) içermektedir.
  • context.Resource
    Yetkilendirme yapılacak kaynağı ifade etmektedir.
  • context.Succeed(requirement)
    Yetkilendirmenin başarıyla tamamlandığını ifade etmektedir.

Evet, yapılan tüm bu çalışmalar neticesinde requirement ve handler sınıflarını uygulamaya aşağıdaki gibi ekleyerek sisteme dahil etmemiz gerekmektedir;

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("UserRoleControl", policy => policy.Requirements.Add(new ResourceAccessRequirement("user")));
});

builder.Services.AddSingleton<IAuthorizationHandler, ResourceAccessHandler>();

Görüldüğü üzere burada politika tabanlı bir kimlik doğrulama yaklaşımı sergilenmektedir. UserRoleControl adında bir politika oluşturulmakta ve gereksinim olarak da oluşturduğumuz ResourceAccessRequirement türü verilmektedir. Burada dikkat ederseniz parametreye ‘user’ değeri verilmiştir. Bu, yukarıdaki satırlarda detaylandırdığımız davranış sürecinde ‘user’a karşılık gelen yetkiyi denetleyecek anlamına gelmektedir. Ayrıca bu politikanın kullanılacağı endpoint’lerde davranışın devreye girebilmesi için ResourceAccessHandler‘ın IoC Container’a IAuthorizationHandler referansına karşın eklenmesi gerekmektedir. Bu adımdan sonra artık hangi endpoint’te bu politika kullanılacaksa aşağıdaki gibi RequireAuthorization‘dan istifade edilerek politika adının belirtilmesi yeterlidir.

app.MapGet("/", () => Results.Ok())
    .RequireAuthorization("UserRoleControl");

İşte bu kadar. Artık bu yapılandırmalar neticesinde uygulama özelleştirilmiş davranışla korunaklı hale getirilmiştir diyebiliriz.

Nihai olarak;
Her ne kadar Asp.NET Core mimarisi, genel geçer senaryolara uygun authentication ve authorization yapılanmalarını standartlar eşliğinde hazır bir şekilde bizlere sunuyor olsa da içeriğimiz süresince görüldüğü üzere bazen çok spesifik durumlara karşın özelleştirilmiş davranışlar sergilememiz gerekebilmektedir ve bu gereksinimlere karşın Asp.NET Core mimarisi tarafından genişletici yapılar ucu açık bir şekilde hizmetimize sunulmuştur. Elbette standartların dışına çıkıp özel kimlik doğrulama ve yetkilendirme davranışlarının geliştirilmesi gerektiği olası ihtiyaç durumlarında kaçınılmaz olarak bu yapılanmalara başvurarak uygulamalarımızı her senaryoya uygun bir şekilde gönül rahatlığıyla geliştirebiliriz.

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

Not : Örnek çalışmaya aşağıdaki github adresinden erişebilirsiniz.
https://github.com/gncyyldz/AspNetCoreCustomAuth

Bunlar da hoşunuza gidebilir...

2 Cevaplar

  1. Mürsel YILDIRIM dedi ki:

    Merhaba hocam,

    bu sayfada anlattğınız “Özelleştirilmiş Authentication ve Authorization” ile Identity Server arasında ne gibi bağlayıcı bir fark vardır? Neden özelleştirilmiş bir Authentication ve Authorization Yapılanmasına ihtiyaç duyarız?

    • Gençay dedi ki:

      Merhaba,

      Aslında bu sorunun cevabını makalede vermiş bulunuyorum.

      • Kullanılan token, JWT değil farklı bir formatta değer olabilir.
      • Token’ın özelleştirilmiş bir şifreleme algoritmasıyla doğrulanması gerekiyor olabilir.
      • Token’da ki payload’ların özel bir işleme tabi tutulması gerekiyor olabilir.
      • vs.

      İyi çalışmalar…

Bir yanıt yazın

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