Yazılım Mimarileri ve Tasarım Desenleri Üzerine

Keycloak’da Advanced Authorization Services (UMA 2.0) Sistemini İnceleyelim… #12

Merhaba,

Bu içeriğimizde, UMA 2.0 standardını temel alan; klasik rol bazlı yetkilendirme mantığından çok daha güçlü bir yapı sunan policy (kural) tabanlı erişim kontrol sistemi olan Advanced Authorization Services sistemini teorik olarak ele alacak ve edineceğimiz farkındalık üzerine Asp.NET Core’da pratiksel deneyimlerde bulunacağız. O halde, konunun temellerini adım adım inceleyerek başlayalım…

Advanced Authorization Services Nedir?

Keycloak Advanced Authorization Services, uygulamalarda erişim kontrolünü basit rol kontrolünden çıkarıp dinamik, kural tabanlı ve bağlama duyarlı hale getiren gelişmiş bir yetkilendirme sistemidir.

Klasik Yetkilendirme Yaklaşımından Farkı Nedir?

Klasik yetkilendirme yaklaşımında kullanıcı sisteme giriş yaptıktan sonra yalnızca token içindeki rolüne bakılarak karar verilir… Buna örnek vermemiz gerekirse eğer; -kullanıcı admin ise izin verilsin, user ise verilmesin- gibi basit kurallar uygulanır. Ancak bu yöntem erişilen verinin ne olduğu, kime ait olduğu ya da hangi koşullarda erişildiği gibi kritik detayları dikkate almadığı için gerçek dünya senaryolarında yetersiz kalabilmektedir.

Buna karşılık Keycloak Advanced Authorization Services, UMA 2.0 temelli yapısıyla yetkilendirmeyi sadece role bağlı olmaktan çıkarıp, kullanıcının kim olduğu, hangi kaynağa erişmek istediği, bu kaynağın kime ait olduğu ve erişimin hangi şartlarda gerçekleştiği gibi dinamik faktörleri değerlendiren policy (kural) tabanlı bir sisteme dönüştürerek çok daha esnek ve güçlü bir kontrol mekanizması sağlamaktadır.

Advanced Authorization; -kim, hangi kaynağa, hangi şartlarda erişebilir?- sorununa policy (kural) ile çözüm getirmektedir.

Sistemin 4 Temel Parçası

Keycloak’un Advanced Authorization yapısında yetkilendirme; korunan varlıkları temsil eden resource, bu varlıklar üzerinde yapılabilecek işlemleri belirleyen scope, erişim kararını veren kuralları içeren policy ve tüm bunların birleşimiyle -kim, hangi kaynağa, hangi şartlarda erişebilir?- sorusuna cevap veren permission bileşenlerinden oluşmaktadır.

Parça Açıklama Örnek
Resource Korunan varlıktır. Yani sistemde erişimini kontrol etmek istediğimiz somut “şey”dir. Kullanıcıların görmesini, değiştirmesini ya da silmesini sınırlandırmak istediğimiz veri veya varlıktır. Örneğin bir sipariş kaydı, bir dosya ya da bir ayar objesi gibi düşünülebilir. Ve Keycloak bu “şey” üzerine kimin ne yapabileceğini belirlemek için onu bir resource olarak tanımlamaktadır. /orders/123
Scope Bir kullanıcının tanımlanan resource üzerinde tam olarak ne yapabileceğini ifade eden yetki tipidir. Yani mesele sadece -erişebilir mi?- değil, -erişince ne yapabilir?- sorusudur! Bu da read, write, delete ve approve gibi aksiyonlarla belirlenmektedir. Böylece aynı kaynağa farklı kullanıcılar farklı seviyelerde yetkiyle erişebilir. read, write
Policy Bir kullanıcının belirli bir resource üzerinde tanımlanan scope’u kullanıp kullanamayacağını belirleyen karar/kural mekanizmasıdır. Sistemin asıl “aklı” burada çalışmaktadır ve kullanıcı rolü, kaynağın sahibi olup olmadığı, kullanıcıya ait özellikler ve zaman gibi faktörlere bakarak erişime izin verir ya da reddeder. Hatta istenirse özel kod yazılarak bu karar tamamen ihtiyaca göre şekillendirilebilir. admin ise izin ver
Permission Resource, scope ve policy’nin bir araya gelerek oluşturduğu nihai karar yapısıdır. Sistemin -bu kullanıcı, şu kaynağa, şu işlemi yapabilir mi?- sorusuna verdiği kesin cevaptır ve esasında tek başına bir şey değildir! Bu üç bileşenin birleşimiyle oluşan ve erişime izin verilip verilmeyeceğini belirleyen nihai noktadır. admin → /orders read

UMA 2.0 Nedir? Ne Getirmektedir?

UMA 2.0 (User-Managed Access 2.0), klasik rol tabanlı yetkilendirmeden çok daha esnek ve kullanıcı odaklı bir erişim kontrol standardıdır. Temel fikir olarak; kaynak (resource) sahiplerinin, kendi kaynaklarına kimin hangi koşullarda erişebileceğini merkezi bir sistem üzerinden yönetebilmesidir. Böylece artık yetkiyi sadece admin, user gibi statik rollere değil, dinamik kurallara ve koşullara bağlı olarak vermeyi sağlamaktadır.

UMA 2.0 ile gelen temel yenilikler şunlardır:

Kısaca UMA 2.0; -kim, neye, ne zaman, hangi koşullarda erişebilir?- sorularını dinamik ve merkezi olarak yönetebilmemizi sağlayan ve özellikle çok kullanıcılı, çok sistemli ve hassas veri ortamlarında kritik bir esneklik ve güvenlik sağlayan bir standarttır.

Nasıl Çalışmaktadır?

UMA 2.0’ın akış mantığı yukarıdaki görselde olduğu gibi cereyan etmektedir:

  1. İstek: Client, API’ye istek atar.
  2. Soru: API, Keycloak’a -bu kullanıcı bu resource’a erişebilir mi?- diye sorar.
  3. Policy Kontrol: Keycloak, policy’leri çalıştırır ve karar üretir.
  4. Sonuç: Nihai olarak erişimi izin verilir yahut engellenir.

Tabi bu şema client bazlı mantığı yansıtmaktadır. Bir yandan kullanıcı bazlı mantığı da ele alırsak;şeklinde bir akış düşünebiliriz. Burada süreci gözlemlersek;

  1. Kullanıcı login olup, access token alır.
  2. Ardından API’ye gidip -ben bu resource’a erişmek istiyorum- der.
  3. Keycloak gerekli kontrolleri sağlar ve izin var mı yok mu değerlendirir. Eğer izin yoksa permission ticket üretir, varsa da ticket’a gerek kalmaksızın RPT üretilir.
    Permission Ticket Nedir?
    Bu ticket, -bu kullanıcı şu resource’a erişmek istiyor- bilgisini taşıyan geçici bir izin talebi belgesidir. Keycloak, bu ticket ile client tekrar geldiği taktirde izin sürecini başlatmaktadır.

    Burada -izin yoksa direkt reddet geç- mantığında bir davranış bekleyebilirsiniz. Ancak UMA 2.0’ın amacı klasik sistemlerden farklı olarak reddetmek odaklı değil, izin sürecini yönetmek odaklıdır. Klasik sistemlerde izin yoksa eğer 403 status code’uyla süreç sonlandırılır. Ancak UMA 2.0’da izin olmasa dahi belki alınabileceği düşünüldüğü için süreç başlatılır. Nasıl belki alınabilir hoca la… diye sorarsanız, düşünün… Kaynak sahibi belki izin verecektir, belki de erişimi admin onaylayacaktır ya da sistem koşullara göre izni sağlayacaktır. Haliyle bu şekilde opsiyonel olan bir süreci değerlendirmek, evet, adı üzerinden bir süreç başlatmayı gerektirmektedir. Bunu da yukarıda söylendiği gibi permission ticket ile gerçekleştirmekteyiz.

    Tabi permission ticket davranışı;

    • kullanıcılar arası veri paylaşımı süreçlerinde,
    • dosya / belge sistemlerinde,
    • sosyal platformlarda,
    • AI agent’ların kullanıcılar adına işlem yaptığı süreçlerde,
    • ve dinamik izin akışlarında

    oldukça mantıklıdır.

    Bunların dışında tabi ki de basit CRUD API’lerinde yahut sadece admin veya user gibi basit ayrımların ve statik rollerin olduğu sistemlerde kullanımı oldukça gereksizdir!

  4. Client, üretilen permission ticket ile tekrar Keycloak’a gider.
  5. Keycloak, policy’leri çalıştırır ve uygunsa RPT üretir.
    RPT (Requesting Party Token) Nedir?
    RPT, UMA 2.0 kapsamında üretilen, kullanıcının belirli bir resource’a hangi izinlerle erişebileceğini gösteren özel bir access token’dır. Yani -bu kullanıcı şunları yapabilir- bilgisini taşıyan token’dır. Ama dikkat edin ki bir access token değildir! Access token ile RPT evet… birbirlerine benzemektedirler, ikisi de JWT’dir, evet… ikisi de Keycloak tarafından üretilir, evet… her ikisi de request süreçlerinde authorization header’ı üzerinden API’lere gönderilir… Ancak bilinmelidir ki farklıdırlar 🙂 Access token’ın amacı kimlikken (authentication), RPT’nin yetkidir (authorization). Access token, -ben kimim- sorusuna cevap verirken, RPT ise -ne yapabilirim- sorusuna cevap vermektedir. Haliyle bir çalışmada access token olmaksızın RPT alınması mümkün değildir!

UMA 2.0’ı Kullanırken Nelere Dikkat Etmek Gerekmektedir?

UMA 2.0 doğru kullanılmadığı taktirde aşırı karmaşık bir canavara dönüşebilir. Bu nedenle konuya ilişkin pratik ve net uyarılarla dikkat edilmesi gereken hususlara dair farkındalık oluşturmak önem arz etmektedir.

13 Kritik Soru / 13 Kritik Cevap

Şimdi konuyu daha iyi anlayabilmek ve duvar köşelerindeki kırıntılara kadar sindirebilmek için akla gelen gelmeyen tüm soruları burada masaya yatırıp cevaplandırmaya çalışalım:

Permission Explosion nedir?
Permission Explosion (İzin Patlaması), Keycloak gibi sistemlerde her kullanıcı, her resource ve her işlem için ayrı ayrı izin tanımlandığında ortaya çıkan kontrolsüz büyüme problemidir. Bu durumda permission sayısı katlanarak artar, sistem yönetilemez hale gelir, performans düşer ve token’lar şişer. Bu yüzden doğru yaklaşım her ihtimal için ayrı izin üretmek yerine daha genel resource ve scope’lar tanımlayıp erişim kararını dinamik policy’lerle vermektir.
Soru 1 | UMA 2.0 ve normal access token arasındaki fark nedir?

Kısaca ifade etmek gerekirse, UMA 2.0 kapsamında access token ve RPT birbirini tamamlayan iki farklı roldedir diyebiliriz. Access token, kullanıcının kim olduğunu kanıtlar, yani sisteme -ben şu kullanıcıyım- diyerek bir kimlik doğrulama sağlar. Ancak kimliğin doğrulanması tek başına bir kaynağa erişim izni vermeyeceğinden yetkilendirme süreçlerinde yeterli değildir. Kullanıcı, belirli bir resource’a erişmek isterse, sistem bu talebi ayrıca değerlendirir ve uygun görürse RPT üretir. RPT, artık kullanıcının hangi kaynağa, hangi işlemlerle erişebileceğini açıkça belirten yetkiyi temsil eder. Bu yüzden akışta önce access token alınır, ardından gerekirse RPT üretilir ve böylece gerçek yetkilendirme kararı RPT üzerinden sağlanmış olur.

Soru 2 | Permission Ticket neden üretilir?

İzin yoksa neden permission ticket üretiliyor?

UMA 2.0 içinde izin yokken permission ticket üretilmesinin sebebi, isteği direkt reddetmek yerine bir izin talebi süreci başlatmaktır. Yani kullanıcı o anda yetkili olmasa bile sistem bu talebi kayıt altına alır, resource sahibi ya da ilgili policy’ler bu talebi sonradan değerlendirebilir ve eğer uygun görülürse kullanıcı tekrar geldiğinde RPT alarak erişim kazanabilir. Böylece sistem statik izin yoksa süreci 403 ile sonlandır yaklaşımı yerine daha dinamik ve yönetilebilir bir yetkilendirme akışı sunmuş olur.

Soru 3 | Resource ve Scope kavramları nasıl tasarlanmalıdır?

Resource scope tasarımı mümkün mertebe sade ve dengeli olmalıdır. Resource’lar her bir veri kaydını tek tek temsil edecek kadar detaylı değil, mantıklı gruplar halinde tanımlanmalı, scope’lar ise gereksiz çeşitliliğe kaçmadan read, write, delete gibi temel aksiyonlarla sınırlandırılmalıdır. Çünkü bu yaklaşım, hem token’ların şişmesini engeller hem de policy’lerin daha anlaşılır, yönetilebilir ve performanslı çalışmasını sağlar.

Soru 4 | Policy oluştururken nelere dikkat etmeliyiz?

Policy tasarlarken kuralların mümkün olduğunca basit, deterministik ve tek bir amaca odaklı olması tavsiye edilmektedir. Her policy tek bir işi çözmeli, mümkünse token içinde claim’ler üzerinden çalışmalı ve karmaşık işlemlerden mümkün mertebe kaçınmalıdır. Yani anlayacağınız klasik yazılımsal ilkelerdeki hassasiyet, policy’lerde de geçerliliğini korumaktadır.

Soru 5 | UMA’yı hangi durumlarda kullanmak mantıklıdır?

UMA 2.0 özellikle kullanıcılar arası veri paylaşımının olduğu ve izinlerin statik rollerle değil dinamik süreçlerle belirlendiği durumlarda daha anlamlıdır. Örneğin, bir kullanıcının kendi dosyasını başkalarıyla paylaşabilmesi, erişim talebinde bulunulup sonradan onay verilmesi veya yetkilerin geçici olarak devredilmesi gibi senaryolarda UMA ciddi bir esneklik sağlamakta, ancak admin/user gibi basit ayrımların yeterli olduğu sistmelerde gereksiz karmaşıklığa sebebiyet vermektedir.

Soru 6 | Her request’te yetkilendirmeyle ilgili durumu Keycloak’a mı soracağım?

Hayır, bu sistemi öldürür. UMA’da alınan RPT cache’lenmeli ve mümkün olduğunca backend tarafında doğrulanmalıdır. Aksi halde her istekte ekstra roundtrip performans darboğazı kaçınılmazdır!

Soru 7 | RPT’yi doğrulamazsak ne olur?

Sistem çöker 🙂 Çünkü direkt client tarafına güvenilmiş olunur. Bu yüzden RPT mutlaka backend tarafında validate edilmeli ve içindeki permission’lar gerçekten kontrol edilmelidir.

Soru 8 | Permission Explosion (İzin Patlaması) nasıl önlenir?

Her resource + scope kombinasyonunu ayrı ayrı üretirseniz sistem çöker. Bunun yerine genelleştirilmiş resource’lar + dinamik policy’ler kullanılmalıdır. Tabi bu durumun pratik düzlemde daha net anlaşılacağı aşikardır 😉

Soru 9 | UMA gerçekten gerekli mi, yoksa overengineering mi?

Eğer sistemde kullanıcılar arası etkileşim yoksa ve izinler statikse yüksek ihtimal overengineering yapılmaktadır. UMA 2.0 ancak dinamik yetki akışı olan senaryolar için anlamlıdır.

Soru 10 | Authorization ile business logic sınırı nerede başlamalı, nerede bitmelidir?

En kritik ayrımlardan birisi de budur diyebiliriz. Keycloak sadece -erişebilir mi?- sorusunu cevaplamalıdır… -Ne yapmalı?- sorusu ise uygulama logic’inde kalmalıdır… Aksi taktirde tüm iş kuralları Keycloak’a gömülürse sistem kontrol edilemez hale gelecektir.

Soru 11 | Eventual consistency problemi yaşanılır mı?

Muhtemelen… Çünkü token (özellikle RPT) üretildikten sonra içindeki izinler bir süre geçerli kalacaktır. Bu yüzden bir kullanıcıdan izin kaldırılsa bile eski token ile bir süre erişim devam edecektir. Bunu çözmek için token ömrü kısa tutulmalı ve re-check mekanizması kurulmalıdır.

Soru 12 | Revocation (izin geri alma) nasıl yönetilir?

UMA’da bu kritik bir problemdir. İzin geri alındığında eski RPT’ler hala geçerli olabilir, bu yüzden ya token süresi kısa tutulmalı ya da kritik işlemlerde backend tarafında ekstradan kontroller sağlanmalıdır.

Soru 13 | Policy değişirse mevcut RPT’ler ne olacak?

Aslında bunu önceki sorularda cevaplandırmış bulunuyoruz… RPT, verilmiş bir izne karşılık gelmektedir. Haliyle policy değişse bile token süresi dolana kadar geçerliliğini koruyacaktır. Bu yüzden sürekli vurguladığımız gibi kısa ömürlü token kullanmanın yanında gerektiği noktalarda token invalidation uygulanması da gerekmektedir.

Keycloak + Asp.NET Core → Policy-Based Authorization

Evet… Artık konuya dair pratik dokunuşlara geçebiliriz. Tabi ilk olarak Keycloak eşliğinde Asp.NET Core’da Policy-Based Authorization‘ı ele alalım istiyorum. Bunun nedeni, Asp.NET Core’daki politika yapılanmasının statik veya yarı-dinamik kontroller eşliğinde yetkilendirme davranışını sağlıyor oluşudur. Bu yaklaşımda, token’dan gelen bilgilerle belirli davranışsal şartlar kontrol edilerek endpoint’lere erişim sınırlandırmaları getirilebilmektedir. Şöyle ki; application-client adında Direct access grants akışını destekleyen bir client’ımız olsun. Bu client’tan alınan token ile aşağıdaki gibi bir politika tabanlı kontrol sağlanabilir.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        options.Authority = "http://127.0.0.1:8080/realms/master";
        options.Audience = "account";
        options.RequireHttpsMetadata = false;

        options.Events = new JwtBearerEvents
        {
            OnTokenValidated = context =>
            {
                var identity = (ClaimsIdentity)context.Principal!.Identity!;
                var realmAccess = context.Principal.FindFirst("realm_access")?.Value;

                if (realmAccess != null)
                {
                    var doc = JsonDocument.Parse(realmAccess);
                    if (doc.RootElement.TryGetProperty("roles", out var roles))
                        foreach (var role in roles.EnumerateArray())
                            identity.AddClaim(new Claim(ClaimTypes.Role, role.GetString()!));
                }

                return Task.CompletedTask;
            }
        };

        options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
        {
            RoleClaimType = ClaimTypes.Role,
            NameClaimType = ClaimTypes.Name
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy =>
    {
        policy.RequireAuthenticatedUser()
              .RequireClaim(ClaimTypes.Role, "admin");
    });
});
.
.
.

Tabi burada, 36 ile 43. satır aralığında oluşturulan politika gereği payload’daki realm_access alanındaki rollerin değerlendirilmesi gerekmektedir. Ancak, mimaride bir obje içerisindeki dizin içinde arama yapacak kısa yol olmadığı için 12 ile 27. satır aralığında ilgili alandaki roller elde edilerek identity’e ClaimTypes.Role type’ı karşılığında verilmektedir, ki AdminOnly politikasında, istek gönderen kullanıcının admin rolüne sahip olup olmadığı değerlendirilebilsin.

Burada ekstradan 12 ile 27. satır aralığındaki claim ayıklama işlemini aşağıdaki gibi harici bir transformation sınıfında yürütebilir ve böylece daha temiz bir çalışma gerçekleştirebiliriz.

    public class KeycloakRolesTransformer : IClaimsTransformation
    {
        public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
        {
            var identity = (ClaimsIdentity)principal.Identity!;
            var realmAccess = identity.FindFirst("realm_access");

            if (realmAccess is not null)
            {
                var json = JsonDocument.Parse(realmAccess.Value);
                if (json.RootElement.TryGetProperty("roles", out var roles))
                    foreach (var role in roles.EnumerateArray())
                    {
                        var r = role.GetString();
                        if (!string.IsNullOrEmpty(r))
                            identity.AddClaim(new Claim(ClaimTypes.Role, r));
                    }
            }

            return Task.FromResult(principal);
        }
    }
builder.Services.AddScoped<IClaimsTransformation, KeycloakRolesTransformer>();

İşte bu kadar…

Keycloak + Asp.NET Core → Resource-Based Authorization (RBA)

Şimdi de esas konumuz olan UMA 2.0’ın davranışına daha yakın olan Resource-Based Authorization’ı tecrübe edelim. Tabi öncelikle ne olduğunu izah edelim…

Resource-Based Authorization Nedir?
Resource-Based Authorization’ın ne olduğunu anlayabilmek için öncelikle normal authorization yapılanmasını anlamlandırmamız gerekmektedir. Malumunuz, normal authorization -bu user admin mi?- şeklinde soru sorar ve evet/hayır mantığında davranış sergiler. Resource-Based Authorization’da ise sorunun mahiyeti biraz değişecektir ve -bu user bu spesifik kaynağa (resource) erişebilir mi?- formuna bürünecektir. Bu kaynak kullanıcının kendi oluşturduğu postu ya da siparişi olabilir. Yani anlayacağınız, erişilmesi gereken resource bazlı bir koşul ortaya koyulması gereken senaryolarda bu yaklaşım oldukça idealdir.

Şimdi bu mantıkta bir authorization çalışması için aşağıdaki gibi yapıların tanımlanması gerekmektedir:

Evet… Resource-Based Authorization’da bundan ibarettir…

Keycloak + Asp.NET Core → UMA 2.0

Artık nihai olarak esas konumuzla pratik yüzleşmeye geldik diyebiliriz…

Yukarıdaki Resource-Based Authorization yaklaşımında kimin hangi kaynağa erişeceği belirlenmektedir. Bunu yaparken, kurallar kodda ya da policy’de olacak şekilde backend’de tanımlama gerçekleştirdik. Yani tüm kontrol tamamen yazılan authorization logic’te tutulmaktadır.

UMA 2.0’da ise kaynak sahibi, kimin neye erişebileceğine kendisi karar verecektir. Bunda ise yetkiyi uygulama değil, resource owner yönetecektir. Haliyle dinamik olarak yetki değişikliği ve kontrolü sağlanabilir.

Resource-Based Authorization
  • Bu kullanıcı bu document’in owner’ı mı?
  • Bu user bu resource’a ait mi?
  • Bu token’a sahip kişi, şu scope’lara erişebilir.
coarse-grained, yani daha az detay, daha genel bir yaklaşım…
UMA 2.0
  • Ahmet bu dosyayı görebilir.
  • Mehmet sadece okuyabilir.
  • Şuayip, Hilmi’nin “123” numaralı faturasına (resource’una/kaynağına) okuma yapabilsin mi?
fine-grained, yani daha fazla detay, daha hassas yaklaşım…

OAuth 2.0’da kararı sunucu verirken, UMA’da ise kaynak sahibi verir, Keycloak ise bu kararı uygular.

Şimdi UMA 2.0’ı uygulayabilmek için adım adım hem Keycloak’da hem de uygulama tarafında hassas çalışmalar gerçekleştirmemiz gerekmektedir. Bu yetkilendirme yaklaşımını tam olarak kurgulayabilmek için burada yapacağımız yapılandırmalar oldukça önem arz etmektedir.

Tabi bu yapılandırmalara geçmeden önce UMA’nın getirdiği konseptteki terminolik kavramları masaya yatırmakta fayda görmekteyim;

Kavram Açıklama
Resource Owner Kaynağın sahibi (örn: Hilmi, kendi faturalarının sahibi)
Resource Server (RS) API’nin kendisi — kaynakları koruyan sunucu
Authorization Server (AS) Keycloak — kimin neye erişebileceğine karar veren

Bunların yanında UMA’yı uygularken her adımda karşımıza çıkabilecek üç token yapısını da tekrar özetleyip notumuzu alalım;

Token Açıklama
PAT (Protection API Token)
  • Resource Server’ın (API’nin) Keycloak’a -ben buyum- demek için kullandığı token’dır.
  • API, Keycloak’a resource kaydetmek için bu token’ı kullanmaktadır.
  • Client Credentials grant ile alınır (kullanıcı yok, makine-makine)
Permission Ticket
  • -Bu kaynağa erişmek istiyorum- talebinin makbuzudur.
  • RS (API), client’a -git Keycloak’tan bununla izin al- der.
  • Tek kullanımlıkdır, kısa ömürlüdür.
RPT (Requesting Party Token)
  • Permission Ticket’ı Keycloak’a götürünce alınan asıl yetki token’ıdır.
  • İçinde hangi kaynağa hangi scope ile erişilebildiği yazmaktadır.
  • API bunu doğrular ve erişime izin verir ya da vermez.

Hadi başlayalım…

UMA’yı Custom Authorization Requirement + Handler Yoluyla Uygulama (En Doğru Yol)

Yukarıdaki çalışmada UMA’yı uygularken permission’daki scope kontrolünü de manuel bir şekilde sağlamaktayız. Halbuki ideal olan bunu Asp.NET Core’un native yetkilendirme pipeline’ına yansıtabilmek ve orada bir policy mantığında gerçekleştirebilmektir.

Bunun için aşağıdaki gibi custom bir authorization requirement oluşturarak süreci daha efektif hale getirebiliriz:

    public record UmaPermissionRequirement(string ResourceName, string Scope) : IAuthorizationRequirement;

Devamında ise bu requirement’ı aşağıdaki handler ile doğrulayabilir ve RPT kontrolünü sağlayabiliriz:

    public class UmaPermissionHandler(RptValidator rptValidator) : AuthorizationHandler<UmaPermissionRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UmaPermissionRequirement requirement)
        {
            if (context.Resource is not HttpContext httpContext)
            {
                context.Fail();
                return Task.CompletedTask;
            }

            var authorizationHeader = httpContext.Request.Headers.Authorization.ToString();

            //Hiç token yoksa başarısız authorization'ın detaylarıyla birlikte Fail'liyoruz
            if (string.IsNullOrEmpty(authorizationHeader) || !authorizationHeader.StartsWith("Bearer "))
            {
                context.Fail(new AuthorizationFailureReason(this, "no_token"));
                return Task.CompletedTask;
            }

            var token = httpContext.Request.Headers.Authorization.ToString().Replace("Bearer ", "");

            //Eğer token varsa RPT'mi değil mi kontrol ediyoruz
            var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
            var jwt = jwtSecurityTokenHandler.ReadJwtToken(token);
            if (!jwt.Claims.Any(c => c.Type == "authorization"))
            {
                context.Fail(new AuthorizationFailureReason(this, "not_rpt"));
                return Task.CompletedTask;
            }

            //RPT ise permission'ları kontrol ediyoruz.
            if (rptValidator.HasPermission(token, requirement.ResourceName, requirement.Scope))
                context.Succeed(requirement);
            else
                context.Fail();

            return Task.CompletedTask;
        }
    }

Devamında ise authorization’ın yalnızca başarısız olduğu durumlarda işlem yapacak olan aşağıdaki gibi bir middleware ile de gerekli kontrolleri sağlayabilir ve fail sebebi no_token yahut not_rpt ise permission ticket alabilir yahut permission_denied ise de 403 döndürebiliriz:

    public class UmaAuthorizationMiddlewareResultHandler(UmaTokenService umaTokenService) : IAuthorizationMiddlewareResultHandler
    {
        private readonly AuthorizationMiddlewareResultHandler _default = new();
        public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
        {
            //Authorization başarılıysa direkt geçiriyoruz
            if (authorizeResult.Succeeded)
            {
                await next(context);
                return;
            }

            //Hatalıysa nedenine bakıyoruz
            var reason = authorizeResult.AuthorizationFailure?.FailureReasons.FirstOrDefault()?.Message;

            //RPT varsa ama yetersiz permission'sa → 403
            if (reason == "permission_denied")
            {
                context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
                await context.Response.WriteAsJsonAsync(new
                {
                    error = "forbidden",
                    message = "Bu resource için yetkiniz bulunmamaktadır."
                });
                return;
            }

            //Token yoksa ya da RPT değilse → 401
            if (reason is "no_token" or "not_rpt")
            {
                //Hangi endpoint için ticket isteniyorsa bunu requirement'tan almak için policy'e bakıyoruz
                var requirement = policy.Requirements.OfType<UmaPermissionRequirement>().FirstOrDefault();

                if (requirement == null)
                {
                    context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                    return;
                }

                var ticket = await umaTokenService.GetPermissionTicketAsync(requirement.ResourceName, requirement.Scope);

                if (ticket == null)
                {
                    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    return;
                }

                context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                context.Response.Headers[HeaderNames.WWWAuthenticate] = $""" UMA realm="master", as_uri="http://localhost:8080/realms/master", ticket="{ticket}" """;

                await context.Response.WriteAsJsonAsync(new
                {
                    error = "uma_redirect",
                    message = "Permission ticket alındı. Bu ticket ile RPT edinin!",
                    ticket = ticket,
                    token_endpoint = "http://localhost:8080/realms/master/protocol/openid-connect/token"
                });
            }

            //Token süresi dolması vs. gibi diğer durumlar için default davranışı devreye alıyoruz
            await _default.HandleAsync(next, context, policy, authorizeResult);
        }
    }

Tüm bu işlemlerden sonra tek yapılması gereken aşağıdaki gibi pipeline’a uygun bir şekilde scope’lar üzerinden politikaların oluşturulması ve endpoint’te kullanılmasıdır…

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("document:read", policy =>
        policy.Requirements.Add(new UmaPermissionRequirement("Document Resource", "read")));

    options.AddPolicy("document:write", policy =>
        policy.Requirements.Add(new UmaPermissionRequirement("Document Resource", "write")));
});
app.MapGet("/api/documents/{id}", (string id) =>
{
    //Buraya gelindiyse middleware RPT'yi doğrulamış demektir...
    return Results.Ok(new
    {
        id,
        title = "Document",
        content = "Bu içerik UMA 2.0 ile korunmaktadır.",
        accessedAt = DateTime.UtcNow
    });
}).RequireAuthorization(policyNames: "document:read");

İşte bu kadar basit…

UMA 2.0 akışının ve RPT’nin davranışsal olarak görsel özeti…

Tabi burada, uygulama zemininde dikkat etmenizde fayda göreceğim bazı tecrübevi noktalar mevcuttur. Bunları sizler yaşadıkça anlayacak ve sindirecek olsanız da yine hafiften önden vurgulamakta fayda görüyorum…

UMA 2.0 senaryolarında, RPT alım sürecinde access_denied hatası yüksek olasıdır. Burada ilk bakacağınız nokta her daim permission’ın policy’lerinin doğrulanıp doğrulanmadığı olmalıdır. Ayrıca 401 döngüsü de bazen kaçınılmaz olabilmektedir. Burada da kontrol edilen access token içerisinde ‘authorization’ claim’i var mı yok mu tarafınızca manuel kontrol edilmelidir. Ha ayrıca client’ın authorization ayarlarında ‘Remote resource management’ yapılandırmasını aktifleştirme de unutulmamalıdır. Bunlara dikkat edildiği sürece diğer yaşanacak hatalar artık yüksek ihtimal UMA’nın dışında başka nedenlere dayanmaktadır 🙂

Evet, görüldüğü üzere UMA 2.0 gibi kompleks ve hassas yapılandırma gerektiren bir akışı a’dan z’ye tam teferruatlı teorik bir inceleme eşliğinde, pratikte de ele almış ve siz değerli okuyucularımızla birlikte deneyimlemiş bulunuyoruz.

Nihai olarak;
İçerik boyunca, klasik rol bazlı yetkilendirmenin ötesine geçerek UMA 2.0 standardını temel alan Keycloak Advanced Authorization Services’i teorik ve pratik boyutlarıyla ele almış bulunuyoruz. Resource, scope, policy ve permission kavramlarının bir araya geldiği ve dinamik izin akışları ile birlikte hassas erişim kontrolleri gerektiren senaryolarda sunduğu esneklikle öne çıkan bu akışı, Asp.NET Core entegrasyonu üzerinden adım adım, permission ticket ve RPT mekanizmalarının pratikte nasıl işlediğini deneyimleyerek gözler önüne sermiş bulunuyoruz.

Unutulmamalıdır ki, UMA 2.0 her senaryo için uygun değildir! Basit yetkilendirme ihtiyaçlarında gereksiz karmaşıklığa yol açabileceği gibi, doğru kullanıldığında ise merkezi ve dinamik bir yetki yönetimi imkanı sunmaktadır.

İ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 konuya dair örnek çalışmanın kaynak kodlarını ve mimari yapısını içermektedir. Detaylar için GitHub üzerinden incelemede bulunabilirsiniz.


GitHub’da Görüntüle →

Exit mobile version