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

Keycloak’da Derinlemesine Kullanıcı İşlemleri #4

Merhaba,

Bu içeriğimizde; Keycloak platformunda kullanıcı yönetimiyle ilgili sık karşılaşılan ancak genellikle yüzeysel geçilen bazı ek senaryolara odaklanıyor olacağız. Bu senaryolar; Keycloak’ın varsayılan olarak sunduğu kullanıcı bilgilerine nasıl ek alanlar eklenebileceği, kullanıcılara geçici veya kalıcı şifrenin nasıl tanımlanabileceği, Password Grant Type üzerinden kullanıcı doğrulamanın nasıl yapılabileceği ve aktif oturum (session) bilgilerine nasıl erişilebileceği gibi süreçleri kapsamaktadır… Ayrıca, bu süreçler sırasında elde edilen token’ları çözümleyerek detaylı bir şekilde inceleyecek; token içeriği üzerinden userinfo endpoint’ine erişerek kullanıcı bilgilerini nasıl gözlemleyebileceğimizi de ele alıyor olacağız. O halde hazırsanız başlayalım…

Client Credentials İle Client’ı Yetkilendirme

Tüm çalışma sürecinde kullanacağımız token, Asp.NET Core İle REST API Üzerinden Temel Keycloak İşlemleri #3 başlıklı yazımızın ‘Client Credentials İle Access Token Alma’ başlığı altında client credentials yaklaşımıyla edindiğimiz access token olacaktır. İlgili çalışmaya, içeriğimizin en sonunda vereceğimiz GitHub adresindeki projeden erişebilir ve kullanım sağlayabilirsiniz.

Varsayılan Olarak Sunulan Kullanıcı Bilgilerine Ek Alan (Attribute) Ekleme

Keycloak’da bir kullanıcı için varsayılan olarak Username, Email, First Name ve Last Name bilgileri sunulmaktadır. Bunlarla birlikte, bu alanlara ek olarak ihtiyaç doğrultusunda özel (custom) alanlar tanımlamak da mümkündür. Bunun için yapılması gereken aşağıdaki ekran görüntüsünde olduğu gibi ilgili realm üzerinden Realm settingsUser profileCreate attribute kombinasyonunu takip etmektir.Keycloak'da Derinlemesine Kullanıcı İşlemleri #4Evet, Keycloak’da kullanıcılara tanımlanan ek alanlara attribute denmektedir! Bir başka deyişle, Keycloak’da standart alanların dışındaki her şeye / bilgiye karşılık gelen tanımlara attribute denmektedir. Bu kullanıcılarda firstName, lastName vs. gibi ekstradan alanlar olabileceği gibi bir realm’ler de genel konfigürasyon amaçlı veya client’larda da özel metadata tanımlama amaçlı alanlar olabilmektedir.

Keycloak’da attribute; standart alanların dışında her şeye / bilgiye karşılık gelen tanımlardır.

Bir attribute tanımlarken aşağıdaki ekran görüntüsünde olan alanların doldurulması gerekmektedir:Burada aşikar olanların dışında göze çarpan alanları izah etmekte fayda görmekteyim;

  • Multivalued : Tanımlanacak bu alanda birden fazla değer alınacaksa işaretlenmelidir. İşaretlendiği taktirde ilgili attribute bir array’e dönüşecektir.
  • Attribute group : Bu alanın profil ekranında hangi başlık altında görüneceğini belirtir.
  • Enabled when : Bu attribute’un ne zaman devreye (yani token’a) gireceği belirlenir. Attribute her kullanıcıda mı çalışsın, yoksa sadece istenirse mi çalışsın? ayarını gerçekleştirmemizi sağlar. Eğer Always denirse, attribute her zaman aktif olacak ve token’a koşulsuz girecektir. Yok eğer Scope are requested seçilirse attribute ancak ilgili scope istendiği taktirde aktif olacak ve istenmediği taktirde token’a girmeyecektir.
    Bu ayarın amacı, token’ın gereksiz yere şişmesini ve bir yandan da veri ifşasını önlemektir.
  • Required field : Kullanıcı login ya da register olurken bu alanın dolu olmasının zorunlu olup olmamasını ayarlar. Mesela, sonraki adımlarda bahsi geçeceği üzere bir kullanıcının firstname ve lastname alanları boşsa eğer login olunamayacağı durumu işte bu ayarla ilişkilidir.
  • Permission : Alanla ilgili yetkilerin ayarlandığı bölümdür:
    • Who can edit? Bu alanı;
      User ➔ Kullanıcı kendisi değiştirebilir.
      Admin ➔ Sadece admin değiştirebilir.
    • Who can view? Bu alanı;
      User ➔ Kullanıcı görebilir.
      Admin ➔ Admin görebilir.

Örneğin phone-number adında aşağıdaki gibi bir attribute oluşturalım.Tabi bu değerin bir telefon numarası olup olmadığını kontrol edebilmek için bir validation tanımlamamız gerekmektedir. Bunu da aşağıdaki görselde olduğu gibi Validations alanından ekleyebiliriz:Keycloak'da Derinlemesine Kullanıcı İşlemleri #4Eğer ki bu validation’a uygun veri girilmezse Keycloak aşağıdaki ekran görüntüsünde olduğu gibi belirtilen error message’ı döndürecektir:Keycloak'da Derinlemesine Kullanıcı İşlemleri #4Velhasıl, attribute sağ salim tanımlanabildiyse eğer kullanıcı detaylarında aşağıdaki gibi gözüküyor olacaktır:Keycloak'da Derinlemesine Kullanıcı İşlemleri #4Peki tüm bu işlemi Administration REST API üzerinden nasıl gerçekleştirebiliriz?
Evet, Keycloak’ta kullanıcı bilgilerine ekstra alan eklemek arayüzden oldukça rahat. Ancak mevzu bahis programatik olarak bu işi gerçekleştirmekse birazcık süreci aşağıdaki gibi detaya indirgememiz gerekmektedir:

app.MapPost("/realm/{realm}/configure-user-attributes", async (string realm, TokenRequestService tokenRequestService, IHttpClientFactory httpClientFactory, RealmAttributeConfigRequest request) =>
{
    var token = await tokenRequestService.GetAccessTokenAsync();
    using var httpClient = httpClientFactory.CreateClient("keycloak");

    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);

    // 1. Mevcut User Profile yapılandırmasını al
    var getProfileResponse = await httpClient.GetAsync($"/admin/realms/{realm}/users/profile");

    if (!getProfileResponse.IsSuccessStatusCode)
        return Results.BadRequest(new { error = "User Profile yapılandırması alınamadı!" });

    var profileContent = await getProfileResponse.Content.ReadAsStringAsync();
    var existingProfile = JsonSerializer.Deserialize<JsonElement>(profileContent);

    // 2. Mevcut attributes listesini al
    List<object> attributesList = null;

    if (existingProfile.TryGetProperty("attributes", out var attributesElement))
        attributesList = attributesElement
                    .EnumerateArray()
                    .Select(attr => JsonSerializer.Deserialize<object>(attr.GetRawText()))
                    .OfType<object>()
                    .ToList();

    // 3. Yeni attribute'ları ekle
    request.NewAttributes.ForEach(newAttribute =>
    {
        // Validation objesini oluştur
        var validations = new Dictionary<string, object>();

        if (newAttribute.Validations is not null)
            newAttribute.Validations.ForEach(validation =>
            {
                validations[validation.Type.ToLower()] = validation.Type.ToLower() switch
                {
                    "length" => new { min = validation.Min, max = validation.Max },
                    "email" => new { },
                    "pattern" => new { pattern = validation.Pattern, errorMessage = validation.ErrorMessage ?? "Geçersiz format" },
                    "options" => new { options = validation.Options },
                    "uri" => new { },
                    "integer" => new { min = validation.Min, max = validation.Max },
                    "number" => new { min = validation.Min, max = validation.Max }
                };
            });

        var attributeConfig = new Dictionary<string, object>
        {
            ["name"] = newAttribute.Name,
            ["displayName"] = newAttribute.DisplayName ?? newAttribute.Name,
            ["validations"] = validations,
            ["permissions"] = new
            {
                view = new[] { "admin", "user" },
                edit = new[] { "admin", "user" }
            },
            ["multivalued"] = newAttribute.Multivalued
        };

        // Required sadece true olduğunda ekle
        if (newAttribute.Required)
            attributeConfig["required"] = new
            {
                roles = new[] { "user" }
            };

        attributesList!.Add(attributeConfig);
    });

    // 4. Güncellenmiş profile yapılandırmasını hazırla
    var updatedProfile = new
    {
        attributes = attributesList,
        groups = existingProfile.TryGetProperty("groups", out var groups) ? JsonSerializer.Deserialize<object>(groups.GetRawText()) : new { }
    };

    var updateJson = JsonSerializer.Serialize(updatedProfile);
    var updateRequest = new StringContent(updateJson, Encoding.UTF8, MediaTypeNames.Application.Json);

    // 5. User Profile'ı güncelle
    var updateResponse = await httpClient.PutAsync($"/admin/realms/{realm}/users/profile", updateRequest);

    if (updateResponse.IsSuccessStatusCode)
        return Results.Ok(new { message = $"Realm seviyesinde {request.NewAttributes.Count} adet yeni attribute başarıyla tanımlandı!" });

    var errorContent = await updateResponse.Content.ReadAsStringAsync();
    return Results.BadRequest(new { error = errorContent });
});

Burada dikkat edilmesi gereken husus şudur ki; attribute’larla ilgili bir çalışma yapıyorsak eğer önceki attribute’ların hepsini elde edip o şekilde çalışma sergilenmesi gerektiğidir. Haliyle bu mantıkla yukarıdaki kodu değerlendirdiğimizde 9 ile 25. satır aralığında mevcut User Profile yapılandırması elde edilmekte ve 28 ile 69. satır aralığında bu mevcut attribute’larla birlikte yenileri eklenerek Keycloak’a gönderilmektedir…

31 ile 46. satırda ise yeni eklenen attribute’un varsa validation davranışı tespit edilmekte ve 48 ile 68. satır aralığında ise bu attribute’da (varsa) validation ayarları ile birlikte mevcut attribute’ların arasına eklenerek Keycloak’a gönderilmeye hazır bir hale gelinmektedir.

72 ile 76. satır aralığında ise User Profile‘ı güncelleyecek formatta (içerisinde attribute’ların en son hali ve group bilgisi eşliğinde) nesne hazırlanmakta ve 82. satırda bu nesne /admin/realms/{realm}/users/profile endpoint’ine gönderilerek tüm çalışma başarıyla nihayete erdirilmektedir.

İşte biraz önce arayüz üzerinden yaptığımız yapılandırmanın kodsal karşılığı birebir bu şekildedir. Tabi burada permissions yapılandırmasını da client’tan bekleyebilir ve daha opsiyonel bir hale getirebiliriz. Burasını sabit değerlerle bırakıyor ve gerisini sizlere bırakıyorum artık…

Bu endpoint’te beklenen body aşağıdaki gibi olacaktır:

{
  "newAttributes": [
    {
      "name": "phone-number",
      "displayName": "Phone Number",
      "multivalued": false,
      "required": false,
      "validations": [
        {
          "type": "pattern",
          "pattern": "^0\\s\\([5][0-9]{2}\\)\\s[0-9]{3}\\s[0-9]{2}\\s[0-9]{2}$",
          "errorMessage": "Lütfen telefon numarasını doğru giriniz.",
          "options": [
            ""
          ]
        }
      ]
    }
  ]
}

Peki bir kullanıcıdaki attribute değerini nasıl değiştirebiliriz?
Bunun için de yapılması gereken önce kullanıcıyı elde edip, yine mevcut attribute’ları eşliğinde, güncellenecek attribute değerini göndermek…

app.MapPut("/user/{userId}/add-new-attribute/{realm}", async (string userId, string realm, TokenRequestService tokenRequestService, IHttpClientFactory httpClientFactory, AddAttributeRequest request) =>
{
    var token = await tokenRequestService.GetAccessTokenAsync();
    using var httpClient = httpClientFactory.CreateClient("keycloak");

    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);

    // 1. Mevcut kullanıcıyı GET ile çek
    var getUserResponse = await httpClient.GetAsync($"/admin/realms/{realm}/users/{userId}");

    if (!getUserResponse.IsSuccessStatusCode)
        return Results.NotFound(new { error = "Kullanıcı bulunamadı!" });

    var userContent = await getUserResponse.Content.ReadAsStringAsync();
    var existingUser = JsonSerializer.Deserialize<JsonElement>(userContent);

    // 2. Mevcut attributes'ları al
    var existingAttributes = new Dictionary<string, List<string>>();

    if (existingUser.TryGetProperty("attributes", out var attributesElement))
        attributesElement
            .EnumerateObject()
            .Select(attribute => existingAttributes[attribute.Name] = attribute.Value
                                        .EnumerateArray()
                                        .Select(v => v.GetString() ?? string.Empty)
                                        .ToList());

    // 3. Yeni attribute'ları mevcut olanlara ekle
    request.NewAttributes.ToList().ForEach(newAttribute => existingAttributes[newAttribute.Key] = newAttribute.Value);

    // 4. Kullanıcıyı güncelle (tüm attributes ile birlikte)
    var updateUser = new
    {
        firstName = existingUser.GetProperty("firstName").GetString(),
        lastName = existingUser.GetProperty("lastName").GetString(),
        email = existingUser.GetProperty("email").GetString(),
        enabled = existingUser.GetProperty("enabled").GetBoolean(),
        attributes = existingAttributes
    };

    var updateJson = JsonSerializer.Serialize(updateUser);
    var updateRequest = new StringContent(updateJson, Encoding.UTF8, MediaTypeNames.Application.Json);

    var updateResponse = await httpClient.PutAsync($"/admin/realms/{realm}/users/{userId}", updateRequest);

    if (updateResponse.IsSuccessStatusCode)
        return Results.Ok(new { message = "Yeni attribute'lar başarıyla eklendi, mevcut olanlar korundu!" });

    var errorContent = await updateResponse.Content.ReadAsStringAsync();
    return Results.BadRequest(new { error = errorContent });
});

Kodu incelerseniz eğer; 18 ile 26. satır aralığında mevcut tüm attribute’lar çekilmekte, 29. satırda ise güncellenen attribute’lar body’den ayıklanarak mevcutların değerleriyle değiştirilmektedir. Ardından 32 ile 39. satır aralığında mevcut olanlarla birlikte tüm attribute’lar en güncel halleriyle nesneye dönüştürülmekte ve 44. satırda da /admin/realms/{realm}/users/{userId} endpoint’ine gönderilerek güncelleme sağlanmaktadır.

Bu attribute güncelleme sürecinde de body aşağıdaki gibi olacaktır:

{
  "newAttributes": {
    "phone-number": [
      "0 (507) 751 45 93"
    ]
  }
}

Peki bir kullanıcı eklenirken attribute değerleriyle birlikte nasıl eklenebilir?
Mantık aynı:

app.MapPost("/user/add-attributes/{realm}", async (string realm, TokenRequestService tokenRequestService, IHttpClientFactory httpClientFactory, UserRequest request) =>
{
    var token = await tokenRequestService.GetAccessTokenAsync();
    using var httpClient = httpClientFactory.CreateClient("keycloak");

    var user = new
    {
        username = request.Username,
        email = request.Email,
        enabled = request.Enabled,
        firstName = request.FirstName,
        lastName = request.LastName,
        attributes = request.CustomAttributes
    };

    var userJson = JsonSerializer.Serialize(user);
    var userRequest = new StringContent(userJson, Encoding.UTF8, MediaTypeNames.Application.Json);

    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);

    var response = await httpClient.PostAsync($"/admin/realms/{realm}/users", userRequest);

    if (response.IsSuccessStatusCode)
        return Results.Ok(new { message = "Kullanıcı başarıyla oluşturuldu ve özel alanlar eklendi!" });

    var errorContent = await response.Content.ReadAsStringAsync();
    return Results.BadRequest(new { error = errorContent });
});

Kullanıcı oluşturulurken User profile‘daki ekstradan tanımlanmış olan attribute’lara değer gönderebilmek için yukarıdaki gibi bir çalışma yapılması gayet yeterlidir. Misal olarak bu endpoint’e aşağıdaki body’de bir istekte bulunulursa ‘xyz’ kullanıcısı oluşturulurken belirtilen telefon numarasıyla oluşturulacaktır:

{
  "username": "xyz",
  "enabled": true,
  "email": "xyz@gmail.com",
  "emailVerified": true,
  "firstName": "Xyz",
  "lastName": "Zyx",
  "customAttributes": {
    "phone-number": [
      "0 (507) 751 45 92"
    ]
  }
}
Kullanıcıya Geçici (Temporary) ve Kalıcı (Permanent) Şifre Tanımlama

Keycloak’da kullanıcı şifrelerini tanımlarken geçici (temporary) ve kalıcı (permanent) ayrımı, temel olarak güvenlik ve kullanıcı yönetimi politikalarını desteklemek amacıyla varlık göstermektedir. Bu ayrım, kullanıcılara admin tarafından verilen başlangıç şifrelerinin kalıcı olarak kullanmasını önleyerek, daha güvenli bir kimlik doğrulama süreci sağlamaktadır. Geçici şifre, kullanıcıyı ilk girişte şifre değiştirmeye zorlayarak, e-posta gibi güvensiz kanallardan şifre paylaşımı gibi potansiyel güvenlik risklerine karşı korumaktadır. Kalıcı şifre ise, doğrudan uzun vadeli kullanım için ayarlanmakta ve zorunlu değişiklik gerektirmemektedir.

Keycloak’da geçici şifre tanımlanmış bir kullanıcı ilk giriş yaptığında Keycloak tarafından otomatik bir şekilde şifre güncellemeye yönlendirilecek ve yeni şifre girilmediği taktirde oturum açımına izin verilmeyecektir.

Keycloak’da şifre değişimini gerçekleştirebilmek için aşağıdaki endpoint’e PUT isteğinde bulunmak gerekmektedir:

🔗 PUT /admin/realms/{realm}/users/{userId}/reset-password

Body:

{
  "type": "password",
  "value": string,
  "temporary": boolean
}

İstekte aşağıdaki gibi gerçekleştirilebilir:

app.MapPut("/user/{userId}/reset-password/{realm}", async (string userId, string realm, TokenRequestService tokenRequestService, IHttpClientFactory httpClientFactory, SetPasswordRequest request) =>
{
    var token = await tokenRequestService.GetAccessTokenAsync();
    using var httpClient = httpClientFactory.CreateClient("keycloak");

    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);

    var passwordData = new
    {
        type = "password",
        value = request.Password,
        temporary = request.IsTemporary
    };

    var passwordJson = JsonSerializer.Serialize(passwordData);
    var passwordRequest = new StringContent(passwordJson, Encoding.UTF8, MediaTypeNames.Application.Json);

    var response = await httpClient.PutAsync($"/admin/realms/{realm}/users/{userId}/reset-password", passwordRequest);

    if (response.IsSuccessStatusCode)
        return Results.Ok(new { message = $"Şifre başarıyla {(request.IsTemporary ? "geçici" : "kalıcı")} olarak ayarlandı!" });

    var errorContent = await response.Content.ReadAsStringAsync();
    return Results.BadRequest(new { error = errorContent });
});
Password Grant Type İle Kullanıcıyı Doğrulama

Password Grant Type (Resource Owner Password Credentials Grant), OAuth 2.0’da en tartışmalı ve en az tavsiye edilen akış türü olsa da maalesef hala çok yaygın kullanılmakta olduğu için bilinmesi kritik arz eden bir davranışa sahiptir.

Password Grant’da; client’ın kullanıcı bilgilerini (username & password) bildiği / aldığı tek grant tipidir.

Bu akış tipinde login işlemini gerçekleştirebilmek için öncelikle aşağıdaki endpoint’e verilen örnek çalışmadaki gibi bir içerikle POST isteğinde bulunulması gerekmektedir:

🔗 POST /realms/{realm}/protocol/openid-connect/token
app.MapPost("/user/authenticate/{realm}", async (string realm, IHttpClientFactory httpClientFactory, PasswordGrantRequest request) =>
{
    using var httpClient = httpClientFactory.CreateClient("keycloak");

    var formData = new Dictionary<string, string>
    {
        ["grant_type"] = "password",
        ["client_id"] = request.ClientId,
        ["username"] = request.Username,
        ["password"] = request.Password
    };

    if (!string.IsNullOrEmpty(request.ClientSecret))
        formData["client_secret"] = request.ClientSecret;

    if (!string.IsNullOrEmpty(request.Scope))
        formData["scope"] = request.Scope;

    var formContent = new FormUrlEncodedContent(formData);

    var response = await httpClient.PostAsync($"/realms/{realm}/protocol/openid-connect/token", formContent);

    if (response.IsSuccessStatusCode)
    {
        var tokenContent = await response.Content.ReadAsStringAsync();
        var tokenResponse = JsonSerializer.Deserialize<JsonElement>(tokenContent);

        return Results.Ok(new
        {
            accessToken = tokenResponse.GetProperty("access_token").GetString(),
            refreshToken = tokenResponse.TryGetProperty("refresh_token", out var rt) ? rt.GetString() : null,
            expiresIn = tokenResponse.GetProperty("expires_in").GetInt32(),
            tokenType = tokenResponse.GetProperty("token_type").GetString()
        });
    }

    var errorContent = await response.Content.ReadAsStringAsync();
    return Results.Unauthorized();
});

Bu akışta dikkat edilmesi gereken iki husus mevcuttur, biri; hangi client üzerinden kullanıcı adı ve şifreyle bir token talebinde bulunulacaksa, o client’ın Direct access grants authentication flow’u desteklemesi gerekliliğidir. Diğeri ise ilgili kullanıcının şifresinin geçici olmaması durumudur! Aksi taktirde token talebi başarılı olmayacaktır.

Ayrıca kodu incelerseniz client_secret değerinin opsiyonel olduğunu göreceksiniz. Bu client’ın access type’ına göre değişiklik gösterebileceği için opsiyonel yapılmış bir değerdir. Eğer ki client; SPA’ler ya da mobile application’lar gibi public client ise bu tür uygulamalarda client secret güvenli bir şekilde saklanamayacağı için haliyle secret değeri olmaksızın bir çalışma söz konusu olacaktır. Yok eğer backend API’ler yahut server-side web application’lar gibi confidential client ise bu tarz uygulamaların güvenli sunucu ortamında çalışmasından kaynaklı secret değeri güvenle saklanabileceğinden, secret değeriyle çalışma gerçekleştirilecektir.

Kullanıcının Aktif Oturum (Session) Bilgilerine Erişme

Bir kullanıcının aktif oturum bilgilerini elde edebilmek için aşağıdaki endpoint’e GET isteğinde bulunulması yeterlidir:

🔗 GET /admin/realms/{realm}/users/{userId}/sessions
app.MapGet("/user/{userId}/sessions/{realm}", async (string userId, string realm, TokenRequestService tokenRequestService, IHttpClientFactory httpClientFactory) =>
{
    var token = await tokenRequestService.GetAccessTokenAsync();
    using var httpClient = httpClientFactory.CreateClient("keycloak");

    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);

    var response = await httpClient.GetAsync($"/admin/realms/{realm}/users/{userId}/sessions");

    if (response.IsSuccessStatusCode)
    {
        var sessionsContent = await response.Content.ReadAsStringAsync();
        var sessions = JsonSerializer.Deserialize<JsonElement>(sessionsContent);

        return Results.Ok(new
        {
            userId,
            activeSessions = sessions.EnumerateArray().Select(session => new
            {
                id = session.GetProperty("id").GetString(),
                username = session.TryGetProperty("username", out var un) ? un.GetString() : null,
                ipAddress = session.TryGetProperty("ipAddress", out var ip) ? ip.GetString() : null,
                start = session.TryGetProperty("start", out var st) ? DateTimeOffset.FromUnixTimeMilliseconds(st.GetInt64()).DateTime : (DateTime?)null,
                lastAccess = session.TryGetProperty("lastAccess", out var la) ? DateTimeOffset.FromUnixTimeMilliseconds(la.GetInt64()).DateTime : (DateTime?)null,
                clients = session.TryGetProperty("clients", out var cl) ? cl.EnumerateObject().ToDictionary(c => c.Name, c => c.Value.GetString()) : null
            }).ToList()
        });
    }

    if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
        return Results.NotFound(new { error = "Kullanıcı bulunamadı!" });

    var errorContent = await response.Content.ReadAsStringAsync();
    return Results.BadRequest(new { error = errorContent });
});
Access Token’ı Çözümleyelim

Şimdi ise Keycloak üzerinden hangi grant type ile üretilmiş olursa olsun, bir access token’ı çözümleyerek içeriğinde yer alan claim’leri birlikte inceleyelim. Tabi bunun için jwt.io‘dan istifade edeceğimizi söylemeye gerek yok sanırım 🙂
Keycloak'da Derinlemesine Kullanıcı İşlemleri #4Burada claim’lere dikkat ederseniz eğer bazı rollerin default olarak geldiğini göreceksiniz.

Evet, bizler bu örneğimizde gncy username’ine sahip olan kullanıcıya herhangi bir rol tanımlamaksızın token talebinde bulunmuştuk. Ama bakıyoruz ki, hem realm seviyesinde hem de client seviyesinde default olarak tanımlı gelen rolleri gözlemlemekteyiz.

Peki bu yetkiler nereden gelmektedir?
Bu roller Keycloak’un tasarımı gereği otomatik olarak gelmektedir. Şöyle ki;

  • offline_access ve uma_authorization rolleri realm seviyesinden default olarak gelmektedir.
  • manage-account, view-profile ve manage-account-links rolleri ise client seviyesinden default olarak gelmektedir.

Peki neden bunlar default gelmektedir?
Bu yetkiler realm settings kısmında varsayılan olarak ayarlandığı için…Keycloak'da Derinlemesine Kullanıcı İşlemleri #4Ee iyi de hoca, bu görselde default-roles-master ve manage-account-links yetkileri yok! dediğinizi duyar gibiyim… Evet, bunlardan manage-account-links yetkisi manage-account‘la birleştirilmiş olduğu için, dolayısıyla manage-account eklendiğinde otomatik token’a eklenmekte, default-roles-master ise kullanıcıya default olarak eklenen tek rol olduğu için o da haliyle token’da gözükmektedir. Bunların görsellerini paylaşmasam da sizler gidip kendiniz gözlemleyebilirsiniz 🙂

UserInfo Endpoint ile Kullanıcı Bilgilerine Erişim

UserInfo, access token üzerinden kullanıcıya ait bilgileri (claims) okumak için kullanılan standart bir OpenID Connect endpoint’idir. Özellikle password grant, authorization code ve refresh token akışlarında elde edilen token’lardan sonra sıkça kullanılmaktadır.

Keycloak'da Derinlemesine Kullanıcı İşlemleri #4UserInfo endpoint’i ile ilgili token’ın kime ait olduğu bilgisini rahatlıkla edinebilmekteyiz. Şöyle ki; bir önceki başlıkta access token’ı çözümlemiş ve claim’lerini incelemiştik. Hatta dikkat ettiyseniz, çözümlenmiş token görselinde bu token’ın kime ait olduğuna dair özel bilgilerin olduğu yandaki bölüm mevcuttur. İşte bizler, eldeki token’da bulunan bu kullanıcı bilgilerine, token’ı decode etmeye gerek kalmaksızın UserInfo endpoint’inden istifade ederek hızlıca erişebilmekteyiz.

🔗 GET /realms/{realm}/protocol/openid-connect/userinfo
app.MapGet("/user/userinfo/{realm}", async (string realm, IHttpClientFactory httpClientFactory, [Microsoft.AspNetCore.Mvc.FromHeader(Name = "Authorization")] string? authorization) =>
{
    using var httpClient = httpClientFactory.CreateClient("keycloak");

    if (string.IsNullOrEmpty(authorization) || !authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
        return Results.Unauthorized();

    var accessToken = authorization.Substring("Bearer ".Length).Trim();

    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

    var response = await httpClient.GetAsync($"/realms/{realm}/protocol/openid-connect/userinfo");

    if (response.IsSuccessStatusCode)
    {
        var userInfoContent = await response.Content.ReadAsStringAsync();
        var userInfo = JsonSerializer.Deserialize<JsonElement>(userInfoContent);

        return Results.Ok(userInfo);
    }

    if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        return Results.Unauthorized();

    var errorContent = await response.Content.ReadAsStringAsync();
    return Results.BadRequest(new { error = errorContent });
});

Keycloak’da Derinlemesine Kullanıcı İşlemleri #4

UserInfo neticesi…

Misal olarak yukarıdaki örnek çalışmaya göz atarsanız eğer request’in Authorization header key’inden token değeri alınmakta ve ilgili endpoint’e GET isteği eşliğinde gönderilmektedir. Tabi bu isteğin başarıyla dönüş yapabilmesi için ilgili token talep edilirken openid scope’u eşliğinde edilmesi zaruridir. Aksi taktirde 403 status code’u eşliğinde yetkisiz bir işlem yapıldığına dair neticeyle karşılaşılıyor olunacaktır.

Bu endpoint, özellikle frontend tarafında kullanıcı profil bilgilerine ihtiyaç olduğunda oldukça kullanışlı olmaktadır. Ayrıca API Gateway’in de yer yer kullanıcı bilgisi istemesinde de bu endpoint’ten istifade edilmektedir.

Nihai olarak;
Görüldüğü üzere Keycloak, yalnızca bir kimlik doğrulama aracı değil, doğru kullanıldığında son derece esnek ve güçlü bir kullanıcı yönetim platformu sunan bir teknolojidir. Bu yazıda, Keycloak üzerinde çoğu zaman yüzeysel geçilen ancak gerçek hayatta sıkça ihtiyaç duyulan kullanıcı yönetimi senaryolarını derinlemesine ele almış ve kullanıcı attribute’larının hem arayüz hem de Administration REST API üzerinden nasıl yönetileceğini, geçici ve kalıcı şifre tanımlama süreçlerini, Password Grant Type ile kullanıcı doğrulamanın kritik noktalarını ve aktif oturum bilgilerinin nasıl elde edilebileceğini adım adım incelemiş bulunuyor ve böylece kurumsal ölçekte güvenli ve sürdürülebilir bir kimlik altyapısı kurmanın temel taşlarını pratik dokunuşlarla tecrübe etmiş oluyoruz.

Ayrıca üretilen token’ların içeriğini çözümleyerek hangi rollerin ve claim’lerin neden varsayılan olarak geldiğini netleştirmiş, UserInfo endpoint’i üzerinden kullanıcı bilgilerine nasıl erişilebileceğini de uygulamalı olarak göstermiş bulunuyoruz.

Bir sonraki içeriğimizde bu yapıların authorization, role ve scope tasarımını ve daha ileri güvenlik senaryolarda nasıl konumlandırılabileceklerini ele alıyor olacağız.

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

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

Bunlar da hoşunuza gidebilir...

Bir yanıt yazın

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