IdentityServer4 Yazı Serisi #3 – Client Credentials
Merhaba,
IdentityServer4 Yazı Serisinin bu üçüncü makalesinde Client Credential yetki tipi ile gerekli konfigürasyonlar eşliğinde IdentityServer4 uygulaması geliştirecek ve ayağa kaldıracağız. Client Credential; machine to machine kimliklendirme dediğimiz iki uygulama arasındaki etkileşime istinaden kullanılan bir akış türüdür. Kullanıcı(User) kimlik doğrulamasından ziyade sadece client’ın doğrulanması önemliyse(authorization) tercih edilmektedir.
Client Credentials, resource server’ları korumak için IdentityServer4’ı kullanmanın en temel senaryosunu sunar.
Client Credentials Stratejisi
Client Credentials yetki tipinde Auth Server sadece client’lara, sistemde tanımlı olan API’lara(resources) erişim yetkisi vermektedir. Yandaki görüntüde de görüldüğü üzere sistemdeki tüm client’lar(her ne kadar tekil örneklendirilmiş olsada) Auth Server tarafından tanımlanmaktadır. Bu tanımlamada tüm client’lara, birbirlerinden ayırmak ve o anki isteğin hangi client’tan geldiğini anlayabilmek için Auth Server tarafından Client ID ve Client Secret değerleri atanmaktadır. Bu değerleri kullanarak Auth Server’dan token elde eden client, ilgili token’ı kullanarak API’lara istekte bulunacaktır. API’lar ise gelen token’ı kontrol edecek ve sistemdeki Auth Server tarafından dağıtılmış olduğunu anladığı taktirde onaylayarak veri erişimine izin verecektir…
Client Credentials, kullanıcıdan ziyade client’ın doğrulanmasını baz alır.
Client Credentials, client doğrulanması temelli olduğundan dolayı kullanıcı ile ilgili işlemlerin(üyelik sistemi, kullanıcı doğrulama vs.) olmadığı durumlarda tercih edilmektedir. Client’ın doğrulanması, client ile API’lar arasındaki etkileşimin/iletişimin/konuşturmanın bir gereği olacağı için makalemizin ilk paragraflarında değinildiği gibi machine to machine tarifiyle nitelendirilmektedir. Dolayısıyla iki uygulamanın(client – API) kendi aralarında iletişim için yetkilendirmesi durumunu Client Credentials ile gerçekleştirmekteyiz. Peki hocam, user ile ilgili yetkilendirme durumunu nasıl gerçekleştirmekteyiz? şeklinde sorunuzu duyar gibiyim… User ve daha nice durumlarla ilgili işlemlerde kullanacağımız izin tiplerini yazı serimizin devamındaki makalelerde tek tek ele alacağımızı bildiririm. O yüzden acele etmeksizin konumuzda kalmaya özen gösterelim 🙂
Client Credentials’da kullanıcı üyelik sistemi vs. gibi kullanıcıya dair operasyonlar bulunmamaktadır. Sistemdeki yapıların(client – API) birbirleriyle haberleşmesine dayalı bir yetkilendirme yöntemidir.
IdentityServer4 – Client Credentials İle Machine to Machine Kimliklendirme Örneği
Evet, içeriğimizin bu noktasından itibaren artık bir IdentityServer4 uygulaması ayağa kaldıracak ve Client Credentials yetki tipiyle machine to machine yetkilendirmenin nasıl yapıldığını tüm detaylarıyla pratikte inceleyeceğiz.
Tabi her pratiğin küçükte olsa bir teorisi vardır. Şu ana kadar attığımız teorik temellerin dışında IdentityServer4 yapılanmasının da temel kavramları olan “API Resource” ve “API Scope” terimlerini tanımlamadan uygulamaya girişmek, pratikte performansımızı olumsuz etkileyecektir.
- API Resource
Auth Server uygulamasının sorumlu olduğu resource’leri yani API’leri ifade eder. - API Scope
Üretilecek token değerinin API üzerindeki yetki alanını ifade eder. Client, Auth Service üzerinden elde ettiği token’da hangi scope değerlerine sahipse ancak o scope değerlerine sahip olan API’lara istekte bulunabilir.
Bu açıklamalardan sonra artık uygulamamıza başlayabiliriz. Uygulamamız bir banka senaryosu üzerinden işlevsellik gösterecektir. IdentityServer4 uygulaması için ‘AuthServer’ isimli bir API projesi, Resource Servers için ise ‘GarantiAPI’ ve ‘HalkBankAPI’ isimli iki adet API projesi oluşturacağız. Süreçte client olarak Postman’i kullanacağız.
dotnet new webapi --name AuthServer Port : 1000
dotnet new webapi --name GarantiAPI Port : 2000
dotnet new webapi --name HalkBankAPI Port : 3000
Hadi kodlayalım…
- 1. Adım – IdentityServer4(AuthServer) Uygulamasını Geliştirme
Herşeyden önce uygulamamızda IdentityServer4 konfigürasyonunu gerçekleştirebilmek için ilk olarak projeye IdentityServer4 kütüphanesinin yüklenmesi gerekmektedir.dotnet add package IdentityServer4 --version 4.1.1
İlgili kütüphaneyi yükledikten sonra artık IdentityServer4 konfigürasyonuna geçebiliriz.
IdentityServer4; hangi client’ın, hangi yetkilerle(scope), hangi API’lere(resource) erişmek istediğini bilmek ister. Bu yüzden temel konfigürasyon olarak bu olguları tanıtmamız gerekmektedir. Bunun için ‘AuthServer’ uygulamasında ‘Config’ adında bir class tasarlayarak gerekli konfigürasyonu içerisinde gerçekleştirebiliriz.(İnternetteki tüm kaynaklarda benzer çalışma gerçekleştirildiği için geleneğe riayet ediyor, bizde Config dosyası üzerinden çalışmamızı gerçekleştiriyoruz 🙂 )
static public class Config { #region Scopes //API'larda kullanılacak izinleri tanımlar. public static IEnumerable<ApiScope> GetApiScopes() { return new List<ApiScope> { new ApiScope("Garanti.Write","Garanti bankası yazma izni"), new ApiScope("Garanti.Read","Garanti bankası okuma izni"), new ApiScope("HalkBank.Write","HalkBank bankası yazma izni"), new ApiScope("HalkBank.Read","HalkBank bankası okuma izni"), }; } #endregion #region Resources //API'lar tanımlanır. public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource("Garanti"){ Scopes = { "Garanti.Write", "Garanti.Read" } }, new ApiResource("HalkBank"){ Scopes = { "HalkBank.Write", "HalkBank.Read" } } }; } #endregion #region Clients //API'ları kullanacak client'lar tanımlanır. public static IEnumerable<Client> GetClients() { return new List<Client> { new Client { ClientId = "GarantiBankasi", ClientName = "GarantiBankasi", ClientSecrets = { new Secret("garanti".Sha256()) }, AllowedGrantTypes = { GrantType.ClientCredentials }, AllowedScopes = { "Garanti.Write", "Garanti.Read" } }, new Client { ClientId = "HalkBankasi", ClientName = "HalkBankasi", ClientSecrets = { new Secret("halkbank".Sha256()) }, AllowedGrantTypes = { GrantType.ClientCredentials }, AllowedScopes = { "HalkBank.Write", "HalkBank.Read" } } }; } #endregion }
Yukarıdaki kaynak koddaki tanımlanan metotları sırasıyla ele alalım; 5. satırdaki ‘GetApiScopes’ metodu içerisinde API’lar da kullanılacak olan yetkileri barındırmaktadır. 18. satırdaki ‘GetApiResources’ metodu ise sistemdeki API’ları tanımlamakta ve dikkat ederseniz yetki alanları ‘Scopes’ propertysi ile verilmektedir. 29. satırda ise API’ları tüketecek olan client’lar tanımlanmaktadır. Her bir client’ın ‘CliendId’ ve ‘ClientName’ değeri verilmekte ve bununla birlikte ‘ClientSecrets’a verilen değer ‘Sha256’ ile şifrelenerek set edilmektedir. ‘AllowedGrantTypes’ propertysi ile bu client’ların yetki tipinin ne olduğu bildirilmekte ve ‘AllowedScopes’ ile de yetkileri bildirilmektedir.
Konfigürasyon ayarları bu şekilde tasarlandıktan sonra uygulamaya dahil edilmesi gerekmektedir. Bunun için ‘Startup.cs’ dosyası üzerindeki ‘ConfigureServices’ metodu içerisinde aşağıdaki çalışmanın yapılması gerekmektedir.
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryApiScopes(Config.GetApiScopes()) .AddInMemoryClients(Config.GetClients()) .AddDeveloperSigningCredential(); } . . . }
Dikkat ederseniz ‘Config’ sınıfı içerisinde tanımlanan tüm konfigürasyonlar ilgili alanlarda çağrılarak uygulamaya eklenmiştir. Burada ‘AddDeveloperSigningCredential’ metoduna dikkatinizi çekmek istiyorum. Aslında bu metodun muadili olan ‘AddSigningCredential’ metoduda mevcuttur.
İkisi arasındaki temel farkı anlayabilmek için JWT imzalama stratejileri hakkında küçükte olsa bilgi sahibi olunması gerekmektedir. Bu stratejiler temelde Simetrik yahut Asimetrik şifreleme algoritmalarıdır. JWT, içerdiği bilgileri saldırganlara karşı koruyabilmek için Simetrik yahut Asimetrik şifreleme algoritmaları kullanmaktadır.
Simetrik Şifreleme; Şifrelenecek olan bilgiyi deşifre edebilmek için gizli anahtar kullanan ve kriptografi teknikleri içerisinde bilinen en eski lakin bir o kadar da basit bir şifreleme türüdür. Şifrelenen veri gönderen ve alıcı tarafında bulunması gereken gizli anahtar değer aracılığıyla çözülebilmektedir. Hızlı ve efektif bir işlem süresinin olmasından dolayı avantajlıdır. JWT değeri imzalanırken kullanılan değerin aynı zamanda doğrulamak için kullanılması durumudur.
Asimetrik Şifreleme; Şifre ve deşifre mantığını kullanan bir şifreleme yöntemidir. Haberleşen taraflardan her birinde, birbiriyle matematiksel bağı olan ve biri gizli(private-secret) ve bir diğeri açık(public) olan birer anahtar bulunmaktadır. Bu anahtarlardan herhangi biriyle şifreleme yapılırken, diğeriyle şifre çözme işlemi gerçekleştirilmektedir. Gizli anahtar haberleşen taraflardan sadece birinde bulunmaktadır ve diğer taraftaki açık anahtar ile doğrulanır. Böylece şifre çözülmüş olur. Açık anahtar herkes tarafından erişilebilirdir, dolayısıyla içeriği çok rahat incelenebilir. Bu durum bir tehlike arz etmemekte, mühim olan gizli anahtarın deşifre edilemez olması güvenceyi sağlamaktadır. Yani anlayacağınız bilgiler sadece gizli anahtarın sahibi tarafından çözülebilecek şekilde şifrelenmektedir.
Asitmetrik Şifreleme, kapı(public key) ve kilit(private key) modeline uygun bir tasarıma sahiptir.
IdentityServer4 framework’ü JWT’leri imzalamak için Asimetrik Şifreleme’yi kullanmaktadır. Aşağıdaki şemada olduğu gibi ‘AuthServer’ client’tan gelen talep neticesinde token dağıtmadan önce ilgili token’ı private key ile şifreler ve ardından ilgili client’a gönderir. Client bu şifrelenmiş token değeri ile API’a istekte bulunacak ve API’nda bu isteği doğrulaması gerekecektir. Bunun için public key’e ihtiyacı vardır. Dolayısıyla API’da ‘AuthServer’dan public key’i alır. Velhasıl, gelen istekteki private key ile API’da ki public key uyumu kontrol edilir ve doğrulama neticesinde istek başarıyla sonuçlanır.
API bu şekilde bir algoritmayla, gelen istekteki token değerinin ‘AuthServer’da üretilen token olup olmadığını doğrulamış olmaktadır.Nihai olarak; ‘AddDeveloperSigningCredential’ metodu development esnasında private ve public keyleri kendisinin otomatik oluşturacağını ifade eder. ‘AddSigningCredential’ metodu ise production’a çıktığında(örneğin Azure) kullanılacak olan bir seçenektir.
Tüm bu çalışmadan sonra yapılması gereken son işlem ‘UseIdentityServer’ middleware’ini ‘UseAuthorization’ middleware’inden önce çağırmaktır.
public class Startup { . . . public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { . . . app.UseIdentityServer(); app.UseAuthorization(); . . . } }
- 2. Adım – Geliştirilen IdentityServer4(AuthServer) Uygulamasını Ayağa Kaldırma
Geliştirilen ‘AuthServer’ projesinin token üretip üretmediğini denetleyebilmek için test amaçlı ayağa kaldırmamız gerekmektedir.
IdentityServer4 işlevsel açıdan işe yarar bazı endpointleri hali hazırda bünyesinde barındırmaktadır. Bunlardan token talebinde bulunabilmek için/connect/token
endpoint’ini kullanabiliriz.İlgili endpoint’e aşağıdaki body bilgilerini barındıran POST isteğinde bulunulması yeterlidir.
client_id Hangi client’tan talep geldiğini ifade eder. client_secret Client’a ait secret değerini ifade eder. grant_type Yetki tipini ifade eder.
Görüldüğü üzere ilgili alanlara ‘Config’ dosyasında belirtilen konfigürasyonlardaki değerler girilerek istekte bulunulduğunda ilgili client’a ait token değeri başarıyla üretilip, döndürülmektedir.Burada küçük bir noktaya temas etmek istiyorum. IdentityServer4(AuthServer) uygulamasını ayağa kaldırdığımız zaman uygulama içerisinde otomatik olarak ‘tempkey.jwk’ dosyası oluşturulmaktadır. Bu dosyanın korunmaya ihtiyacı yoktur. Silindiği taktirde yine tekrardan otomatik oluşturulmaktadır. İşlevsel açıdan ‘AddDeveloperSigningCredential’ metodunun oluşturacağı keyleri tutmaktadır. İçeriği aşağıdaki gibidir;
Böylece IdentityServer4 uygulamasının başarıyla çalıştığını test etmiş olduk. Şimdi sıra API’ları geliştirmeye gelmiştir…
- 3. Adım – API’ların Geliştirilmesi
‘AuthServer’ uygulamasının koruyacağı API’lerin içeriğini sembolik olarak aşağıdaki gibi geliştirmemiz yeterli olacaktır. Nihayetinde burada API’ların verecekleri hizmetlerden ziyade gelişimsel özellikleri önplanda olacaktır.
GarantiAPI;[Route("api/[controller]/[action]")] [ApiController] [Authorize] public class GarantiBankController : ControllerBase { [HttpGet("{musteriId}")] public double Bakiye(int musteriId) { //.... return 1000; } [HttpGet("{musteriId}")] public List<string> TumHesaplar(int musteriId) { //.... return new() { "123456789", "987654321", "564738291" }; } }
HalkBankAPI;
[Route("api/[controller]/[action]")] [ApiController] [Authorize] public class HalkBankController : ControllerBase { [HttpGet("{musteriID}")] public double Bakiye(int musteriId) { //.... return 500.15; } [HttpGet("{musteriID}")] public List<string> TumHesaplar(int musteriId) { //.... return new() { "135792468", "019283745", "085261060" }; } }
Burada asıl önemli olan nokta, API’ların client’tan gelen request’te ki token’ı nasıl doğrulayacaklarını öğrenmeleri gerekmektedir. Yukarıdaki satırlardan da biliyoruz ki token(access_token) private key ile imzalanmaktadır. İşte imzalanmış bu token API’lar da public key ile doğrulanmalıdır.
API’lar da gelecek token değerlerini doğrulayabilmek için öncelikle ilgili API’lara Microsoft.AspNetCore.Authentication.JwtBearer kütüphanesinin yüklenmesi gerekmektedir. Ardından tümünde aşağıdaki çalışma gerçekleştirilmelidir.
public class Startup { public void ConfigureServices(IServiceCollection services) { . . . services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { //Token'ı yayınlayan Auth Server adresi bildiriliyor. Yani yetkiyi dağıtan mekanizmanın adresi bildirilerek ilgili API ile ilişkilendiriliyor. options.Authority = "https://localhost:1000"; //Auth Server uygulamasındaki 'Garanti' isimli resource ile bu API ilişkilendiriliyor. options.Audience = "Garanti"; }); . . . } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { . . . app.UseAuthentication(); app.UseAuthorization(); . . . } }
Yukarıdaki kaynak kodda ‘GarantiAPI’ uygulamasının konfigürasyonu bulunmaktadır. Benzer uygulamayı ‘HalkBankAPI’ uygulaması içinde gerçekleştirmemiz gerekmektedir. Tabi ki de örneklendirmemizdeki ‘HalkBankAPI’a karşılık geliştirmeyi her ne kadar sizlere bırakmış olsamda, yazılmış halini makalemizin nihayetinde paylaşacağım örnek projede somut olarak inceleyebilirsiniz. Velhasıl, şimdi biz yukarıdaki kaynak kodun mealine odaklanırsak eğer; 9. satırda ‘AddAuthentication’ servisinin uygulamaya dahil edildiğini görmekteyiz ve şema adı olarak ‘JwtBearerDefaults.AuthenticationScheme’ sabitinin taşıdığı ‘Bearer’ değeri verilmektedir. Bu değer(şema adı) uygulamada authentication instance’ını tutan bir nitelik kazanmaktadır. İstek doğrultusunda farklı bir isimde verilebilmektedir. 10. satırı incelersek eğer ‘AddJwtBearer’ servisi eklenerek uygulamada JWT entegrasyonu sağlanmıştır. İçerik olarak yine aynı şema adı bildirilmekte ve böylece uygulamadaki bir önceki metotta eklenen aynı şema adındaki authentication mekanizması JWT ile bağdaştırılmaktadır. Konfigürasyon açısından olayı değerlendirirsek eğer 13. satırda ‘Authority’ bilgisi verilerek bu API’ın hangi Auth Server tarafından korunduğu, başka bir deyişle yetkilisinin kim olduğu bildirilmektedir. 15. satırda ise ‘Audience’ ile Auth Server’da ki hangi resource’e karşılık geldiği bildirilmekte ve böylece API ile ilişkilendirilmektedir. Son olarak 26. satırda ‘UseAuthentication’ ve peşinen ‘UseAuthorization’ middleware’leri çağrılarak uygulama kimlik doğrulamaya genel hatlarıyla hazır hale getirilmektedir.
Artık bu API’a bir istek gelince ‘Authority’de tanımlanmış olan ilgili Auth Server’a gidecek ve public key’i alacaktır. Ardından private key ile imzalanmış olan access token değerini bu public key ile doğrulayacak ve böylece token’ın gerçek bir token olduğunu anlamış olacaktır.
Ayriyetten IdentityServer4 kütüphanesinin konfigürasyon kolaylığına dair dikkatinizi çekmek istiyorum. Normalde Token Bazlı Kimlik Doğrulama(JWT) makalemi incelerseniz eğer bir API’da ki gelen token’ı doğrulayabilmek için ‘SymmetricSecurityKey’ vs. gibi özellikleri belirlememiz gerekmektedir. Halbuki görüldüğü üzere IdentityServer4 ile API’lar da bu gibi konfigürasyonlara gerek duyulmamaktadır.
Test Edelim
Şimdi geliştirilen tüm uygulamaları ayağa kaldırarak test edelim.
Görüldüğü üzere IdentityServer4 uygulamamızın testi başarıyla sonuçlanmıştır. Şimdi son olarak üretilen JWT yapılanmasının nasıl bir içeriğe sahip olduğunu inceleyerek makalemizi noktalayalım.
JWT İncelemesi
Üretilen JWT değerini açabilmek ve üzerinde tutulan değerleri(Payload) inceleyebilmek için decode etmemiz gerekmektedir. Bunun için jwt.io adresini kullanabilirsiniz. İlgili adreste açılan forma elinizdeki JWT değerini verirseniz eğer decode edip, taşıdığı tüm payload’ları sizlere gösterecektir.
Burada JWT içerisindeki şifrelenmiş tüm Payload’ları görebilmek sizler açısından hafif bir kuşkuya mahal vermiş olabilir ve hani burada güvenlik! sorusunu sormanızı sağlayabilir. Evet, JWT’ler bu şekilde decode edilebilmektedir ama server’da ki hashlenmiş secret key’i bilmeden kimse herhangi bir valid değer üretememektedir. Bu da token’ın public olmasına herhangi bir sakınca getirmemektedir.
Velhasıl, yukarıda decode edilmiş olan JWT payload’larına göz atarsak eğer;
görüldüğü üzere tüm bilgiler karşımızdadır. Burada aud(Audience) değeri bu JWT’nin hangi resource tarafından kabul edileceğini, client_id değeri istek yapan client’ın Auth Server’da ki kimlik bilgisini ve scope değeri ise ilgili client’ın taşıdığı tüm yetkilerini bildirmektedir.
Sonuç
Bu makalemizde IdentityServer4 temellerini pratikte tüm detaylarıyla ele almış, bir Auth Server’dan nasıl token talebinde bulunulabileceğini, client’ın elde ettiği token ile ne şekilde API’lara erişim sağlayabileceğini vs. üzerinde uzun uzun istişare ederek örneklendirmiş bulunmaktayız. Bundan sonra herhangi bir client uygulaması üzerinden bu operasyonun nasıl gerçekleştirilebileceğini çok rahat anlayabilecek seviyede temellerin atıldığını düşünmekteyim. Sonraki içeriklerimizde adım adım yetkilendirme boyutunu detaylandıracak ve olayı daha kurumsal uygulamalarda profesyonel çalışmalar yapabilecek noktalara getireceğiz…
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…
Not : Örnek çalışmayı indirebilmek için buraya tıklayınız.
4 Cevaplar
[…] kullanabilmekteyiz. Bu endpoint’lere misal olarak; yazı serimizin 3. makalesi olan Client Credentials başlıklı içeriğimizde, client’ların Auth Server’dan token talep edebilmesi için […]
[…] IdentityServer4 Yazı Serisi #3 – Client Credentials […]
[…] önce IdentityServer4 Yazı Serisinin üçüncü makalesi olan Client Credentials başlıklı makalemizde IdentityServer4 yapılanması üzerinden client’ların […]
[…] Yazı Serisinin üçüncü makalesi olan Client Credentials başlıklı makalede IdentityServer4’ün pratiksel temelleri eşliğinde işlevsel […]