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

OpenIddict #2 – Client Credentials Flow

Merhaba,

Bu içeriğimizde machine to machine kimliklendirme dediğimiz iki uygulama arasındaki etkileşime istinaden kullanılan bir akış türü olan Client Credential’ı OpenIddict kütüphanesi ile nasıl uygulayabileceğimizi inceliyor olacağız.

Client Credentials Flow Nedir?

Sistemde kullanıcılardan ziyade client’lara erişim yetkisi vermemizi sağlayan bir akış türüdür. Bir başka deyişle, kullanıcıdan ziyade client doğrulamasını baz almaktadır.

Örnek Çalışma

İçeriğin teorisini fazla uzatmaksızın konuya hızlıca pratiksel açıdan devam etmek istiyorum. Şimdi OpenIddict kütüphanesi ile Client Credentials Flow’u teknik olarak adım adım örneklendiriyor olacağız. Bunun için önceki makalemizde(bknz : OpenIddict Nedir? ve Authorization Server Nasıl Kurulur?) oluşturmuş olduğumuz Authorization Server uygulamasını github adresinden çekip, aşağıdaki önergeleri sırasıyla tatbik edebilecek temelleri hazır kıta bir vaziyette oluşturmanızı tavsiye ederiz.

  • Adım 1 | (OpenIddict Paketlerinin Yüklenmesi)
    İlk olarak Authorization Server’a OpenIddict kütüphanelerini yükleyerek başlamamız gerekmektedir. Bu kütüphaneler;

    olmak üzere üç tanedirler.

    Kütüphaneleri yükleyebilmek için Nuget’ten destek alabilir ya da isterseniz aşağıdaki CLI komutları üzerinden de talimatlarınızı verebilirsiniz.

    dotnet add package OpenIddict
    dotnet add package OpenIddict.AspNetCore
    dotnet add package OpenIddict.EntityFrameworkCore

    Ayrıca hazır el atmışken uygulama sürecinde SQL Server bağlantısı ve migration yapılanmaları için kullanacağımız şu kütüphaneleri de yüklemekte fayda vardır;

    dotnet add package Microsoft.EntityFrameworkCore.SqlServer
    dotnet add package Microsoft.EntityFrameworkCore.Tools

    OpenIddict #2 – Client Credentials Flow

  • Adım 2 | (OpenIddict Konfigürasyonunun Yapılması)
    Uygulamada OpenIddict işlevselliğinden istifade edebilmek için öncelikle temel bir yapılandırmada bulunmamız gerekmektedir. Bu yapılandırma sürecinde OAuth 2.0 ve OpenID Connect akışları etkinleştirilmelidir. Tabi burada kullanacağımız akışın machine to machine olan Client Credentials olduğunu biliyoruz. Şimdi uygulamanın ‘Program.cs’ dosyasına gelelim ve aşağıdaki konfigürasyonları gerçekleştirelim.

    using Microsoft.AspNetCore.Authentication.Cookies;
    using Microsoft.EntityFrameworkCore;
    using OpenIddict.AuthorizationServer.Models;
    
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddControllersWithViews();
    builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => options.LoginPath = "/account/login");
    
    //OpenIddict servisini uygulamaya ekliyoruz.
    builder.Services.AddOpenIddict()
        //OpenIddict core/çekirdek yapılandırmaları gerçekleştiriliyor.
        .AddCore(options =>
        {
            //Entity Framework Core kullanılacağı bildiriliyor.
            options.UseEntityFrameworkCore()
                   //Kullanılacak context nesnesi bildiriliyor.
                   .UseDbContext<ApplicationDbContext>();
        })
        //OpenIddict server yapılandırmaları gerçekleştiriliyor.
        .AddServer(options =>
        {
            //Token talebinde bulunulacak endpoint'i set ediyoruz.
            options.SetTokenEndpointUris("/connect/token");
            //Akış türü olarak Client Credentials Flow'u etkinleştiriyoruz.
            options.AllowClientCredentialsFlow();
            //Signing ve encryption sertifikalarını ekliyoruz.
            options.AddEphemeralEncryptionKey()
                   .AddEphemeralSigningKey()
                   //Normalde OpenIddict üretilecek token'ı güvenlik amacıyla şifreli bir şekilde bizlere sunmaktadır.
                   //Haliyle jwt.io sayfasında bu token'ı çözümleyip görmek istediğimizde şifresinden dolayı
                   //incelemede bulunamayız. Bu DisableAccessTokenEncryption özelliği sayesinde üretilen access token'ın
                   //şifrelenmesini iptal ediyoruz.
                   .DisableAccessTokenEncryption();
            //OpenIddict Server servislerini IoC Container'a ekliyoruz.
            options.UseAspNetCore()
                   //EnableTokenEndpointPassthrough : OpenID Connect request'lerinin OpenIddict tarafından işlenmesi için gerekli konfigürasyonu sağlar.
                   .EnableTokenEndpointPassthrough();
            //Yetkileri(scope) belirliyoruz.
            options.RegisterScopes("read", "write");
        });
    
    //OpenIddict'i SQL Server'ı kullanacak şekilde yapılandırıyoruz.
    builder.Services.AddDbContext<ApplicationDbContext>(options =>
    {
        options.UseSqlServer(builder.Configuration.GetConnectionString("SQLServer"));
        //OpenIddict tarafından ihtiyaç duyulan Entity sınıflarını kaydediyoruz.
        options.UseOpenIddict();
    });
    
    var app = builder.Build();
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    
    app.UseRouting();
    
    app.UseAuthentication();
    app.UseAuthorization();
    
    app.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
    
    app.Run();
    

    Yukarıdaki kod bloğunu incelersek eğer; 12 ile 42. satır aralığında OpenIddict için temel konfigürasyonların yapıldığını görmekteyiz. Bu konfigürasyonların açıklaması kod içerisinde yapılmış olsa da bizler yine de üzerinden az bir şey geçelim isterim.

    İlk olarak OpenIddict kütüphanesinin Entity Framework Core aracılığıyla SQL Server veritabanında çalışacağını görüyoruz. Burada isterseniz EF Core dışında, standart EF yahut direkt MongoDB yapılandırmasını kullanabilirsiniz. Yapılan veritabanı konfigürasyonundan sonra 49. satırdaki ‘UseOpenIddict’ metodu sayesinde OpenIddict tarafından kullanılacak entity’ler ilgili veritabanına eklenmektedirler. Tabi bunun için birazdan migration oluşturmamız ve migrate etmemiz gerekmektedir.

    12. satırdaki ‘AddOpenIddict’ metodundan sonra 14 ile 20. satır aralığında OpenIddict için gerekli core/çekirdek bileşenler yapılandırılırken ardından 22 ile 42. satır aralığında ise server ile ilgili bileşenler yapılandırılmaktadır. Özellikle bu noktada tasarlanan akışın Client Credentials olduğu belirlenmekte ve oluşturulacak token’ın şifrelenebilmesi ve imzalanabilmesi için gerekli konfigürasyonlar gerçekleştirilmektedir.

    35. satırdaki ‘DisableAccessTokenEncryption’ metodu bizim için oldukça önem arz etmektedir. Çünkü, OpenIddict tarafından oluşturulacak access token sadece imzalanmakla kalmamakta aynı zamanda şifrelenmektedir. Bu şifreleme neticesinde ilgili token değerini jwt.io gibi bir çözümleyici tarafından açıp, inceleyememekteyiz. İşte bu durumdan dolayı ilgili metot ile şifrelemeyi devre dışı bırakabilmekteyiz.

    41. satırdaki ‘RegisterScopes’ metoduna göz atarsanız eğer uygulama bazında yetkilendirme sürecinde kullanılacak izinlerin tanımlandığını göreceksiniz.

  • Adım 3 | (Migration Oluşturma ve Migrate Etme)
    Yapılan bu konfigürasyonlar neticesinde artık migration oluşturarak OpenIddict için gerekli olan entity’lerin tablo modellemesini veritabanına migrate edebilirsiniz.

    add-migration mig_1
    update-database

    OpenIddict #2 – Client Credentials FlowGenerate edilen veritabanını merak ediyorsanız eğer yandaki görseli inceleyebilirsiniz. Görüldüğü üzere OpenIddictApplications, OpenIddictAuthorizations, OpenIddictScopes ve OpenIddictTokens isimlerinde tablolar oluşturulmuştur. Bu tabloların işlevselliğini izah etmemiz gerekirse eğer;

    • OpenIddictApplications, uygulamadaki client bilgilerinin tutulduğu tablodur.
    • OpenIddictAuthorizations, uygulamadaki client’ların yetki durumlarının tutulduğu tablodur.
    • OpenIddictScopes, uygulamadaki scope’ların tutulduğu tablodur.
    • OpenIddictTokens, uygulamada client’lar için üretilen token’ların expire, type vs. gibi bilgilerinin tutulduğu tablodur.
  • Adım 4 | (Yapılandırmanın Sağlıklı Olup Olmadığını Kontrol Etme)
    OpenIddict’in düzgün yapılandırılıp yapılandırılmadığını kontrol etmek için uygulamayı ayağa kaldırıp aşağıdaki endpoint’e istek yapabilirsiniz.

    GET .../.well-known/openid-configuration

    OpenIddict #2 – Client Credentials Flow
    Eğer ki ilgili endpoint’e yapılan istek neticesinde yandaki görseldeki gibi verilerle karşılaşıyorsanız yapılandırmanın gayet başarılı olduğunu söyleyebiliriz.

    Gelen verilere göz atarsanız eğer; token endpoint’inden tutun da, desteklenen grant type’ına ve scope’lara kadar tüm bilgileri bizlerle paylaşmaktadır.

    Ayrıca desteklenen claim’lerin vs. nasıl belirlenebileceğini yazımızın devamında inceliyor olacağız. Adım adım, sabırla tatbik ederek okumaya devam ediniz.

  • Adım 5 | (Client Ekleme Sayfasının Tasarlanması ve İşlemin Gerçekleştirilmesi)
    Şimdi, Authorization Server’a client ekleme işlemlerini gerçekleştiriyor olacağız. Bunun için öncelikle kullanıcıdan client bilgilerini edinecek olan ‘ClientCreateVM’ isimli viewmodel’ı oluşturarak başlayalım.

        public class ClientCreateVM
        {
            public string ClientId { get; set; }
            public string ClientSecret { get; set; }
            public string DisplayName { get; set; }
        }
    

    Ardından ‘ClientsController’ sınıfını oluşturarak içerisini aşağıdaki gibi dolduralım.

        public class ClientsController : Controller
        {
            readonly IOpenIddictApplicationManager _openIddictApplicationManager;
    
            public ClientsController(IOpenIddictApplicationManager openIddictApplicationManager)
            {
                _openIddictApplicationManager = openIddictApplicationManager;
            }
    
            [HttpGet]
            public async Task<IActionResult> CreateClient()
            {
                return View();
            }
    
            [HttpPost]
            public async Task<IActionResult> CreateClient(ClientCreateVM model)
            {
                var client = await _openIddictApplicationManager.FindByClientIdAsync(model.ClientId);
                if (client is null)
                {
                    await _openIddictApplicationManager.CreateAsync(new OpenIddictApplicationDescriptor
                    {
                        ClientId = model.ClientId,
                        ClientSecret = model.ClientSecret,
                        DisplayName = model.DisplayName,
                        Permissions =
                        {
                            OpenIddictConstants.Permissions.Endpoints.Token,
                            OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
                            OpenIddictConstants.Permissions.Prefixes.Scope + "read",
                            OpenIddictConstants.Permissions.Prefixes.Scope + "write"
                        }
                    });
                    ViewBag.Message = "Client başarıyla oluşturulmuştur.";
                    return View();
                }
                ViewBag.Message = "Client zaten mevcuttur.";
                return View();
            }
        }
    

    Burada client ile ilgili işlemleri gerçekleştirebilmek için 3. satırda görüldüğü üzere ‘IOpenIddictApplicationManager’ sınıfından istifade edilmektedir. Bu sınıf aracılığıyla silme, güncelleme, adet(count) vs. gibi işlemleri yürütmekteyiz. 18 ile 39. satır aralığında gerçekleştirilen client ekleme işleminde öncelikle kullanıcıdan gelen client-id bilgisine karşılık bir client’ın olup olmadığı kontrol edilmekte, eğer yoksa client üretilmekte yok eğer varsa hiçbir işlem gerçekleştirilmemektedir. Tabi ki de ben denizin sade tutmaya çalıştığı bu noktayı sizler uygulamanızın davranışına göre özelleştirebilirsiniz.

    27 ile 33. satır aralığında ise bu eklenecek olan client’ların hangi izinlere sahip olacağı belirlenmektedir. Dikkat ederseniz, önceki satırlarda RegisterScopes metodu ile belirlediğimiz izinleri burada da eklenen client’larla eşleştirmekteyiz.

    Bu çalışmadan sonra sırada ‘CreateClient’ action’ının view’ini oluşturmak vardır.

    @model OpenIddict.AuthorizationServer.ViewModels.ClientCreateVM
    @if (ViewBag.Message != null)
    {
        <div class="alert alert-success" role="alert">
            @ViewBag.Message
        </div>
    }
    else
    {
        <form autocomplete="off" asp-route="Clients">
            <div class="card">
                <input type="text" class="form-control form-control-lg" placeholder="Client ID" asp-for="ClientId" autofocus>
                <input type="password" class="form-control form-control-lg" placeholder="Client Secret" asp-for="ClientSecret">
                <input type="text" class="form-control form-control-lg form-control-last" placeholder="Display Name" asp-for="DisplayName" autofocus>
            </div>
            <p>
                <button type="submit" class="btn btn-dark btn-block mt-3">Create Client</button>
            </p>
        </form>
    }
    

    İlgili sayfayı yukarıdaki gibi tasarlayıp, uygulamayı derleyip, çalıştırdığımızda aşağıdaki gibi bir sayfayla karşılaşıyor olacağız.
    OpenIddict #2 – Client Credentials Flow
    Ekrandaki bilgiler eşliğinde(Client Secret : postman-client-secret) ‘Create Client’ butonuna tıklarsanız eğer aşağıdaki gibi başarılı olduğuna dair mesaj alıyor olacaksınız.
    OpenIddict #2 – Client Credentials Flow
    Ayrıca veritabanına da göz atarsak eğer;
    OpenIddict #2 – Client Credentials Flowgörüldüğü üzere başarıyla client’ın eklendiğini gözlemlemekteyiz. Bu arada client-secret değerinin şifrelenerek tutulduğuna dikkatinizi çekerim 😉

  • Adım 6 | (Token Endpoint’inin Tasarlanması)
    Şimdi sıra en önemli yere geldi diyebiliriz, token endpoint’inin tasarlanmasına… Hatırlarsanız yukarıdaki satırlarda, OpenIddict server yapılandırmalarını sağladığımız noktalarda SetTokenEndpointUris fonksiyonu aracılığıyla token endpoint’inin /connect/token adresine karşılık geleceğini belirtmiştik. Şimdi bu adrese karşılık controller çalışması gerçekleştirecek ve client’ların token edinmesini sağlıyor olacağız.

        public class AuthorizationController : Controller
        {
            readonly IOpenIddictApplicationManager _applicationManager;
    
            public AuthorizationController(IOpenIddictApplicationManager applicationManager)
            {
                _applicationManager = applicationManager;
            }
    
            //Bu action'ın endpoint'ini token endpoint ile aynı şekilde ayarlıyoruz.
            [HttpPost("~/connect/token")]
            public async Task<IActionResult> Exchange()
            {
                var request = HttpContext.GetOpenIddictServerRequest();
                if (request?.IsClientCredentialsGrantType() is not null)
                {
                    //Client credentials OpenIddict tarafından otomatik olarak doğrulanır.
                    //Eğer ki gelen request'in body'sindeki client_id veya client_secret bilgileri geçersizse, burası tetiklenmeyecektir.
    
                    var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
                    if (application is null)
                        throw new InvalidOperationException("This clientId was not found");
    
                    var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    
                    //Token'a claim'leri ekleyelim. Subject'in eklenmesi zorunludur.
                    //Destination'lar ile claim'lerin hangi token'a ekleneceğini belirtiyoruz. AccessToken mı? Identity Token mı? Yoksa her ikisi de mi?
                    identity.AddClaim(Claims.Subject, (await _applicationManager.GetClientIdAsync(application) ?? throw new InvalidOperationException()), Destinations.AccessToken, Destinations.IdentityToken);
                    identity.AddClaim(Claims.Name, (await _applicationManager.GetDisplayNameAsync(application) ?? throw new InvalidOperationException()), Destinations.AccessToken, Destinations.IdentityToken);
                    identity.AddClaim("ozel-claim", "ozel-claim-value", Destinations.AccessToken, Destinations.IdentityToken);
    
                    identity.AddClaim(JwtRegisteredClaimNames.Aud, "Example-OpenIddict", Destinations.AccessToken, Destinations.IdentityToken);
    
                    var claimsPrincipal = new ClaimsPrincipal(identity);
                    claimsPrincipal.SetScopes(request.GetScopes());
    
                    //SignIn return etmek, biryandan OpenIddict'ten uygun access/identity token talebinde bulunmaktır.
                    return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
                }
                throw new NotImplementedException("The specified grant type is not implemented.");
            }
        }
    

    Yukarıdaki ‘AuthorizationController’ isimli sınıfı ve içerisindeki ‘Exchange’ adını verdiğimiz action’ın davranışını incelerseniz eğer yine kod içerisinde gerekli açıklamaları yapmış olsakta hafiften burada da izah amaçlı neler yaptığımıza temas etmekte fayda görmekteyim.

    Herşeyden önce 11. satıra göz atarsanız eğer bu ‘Exchange’ isimli action’ı token endpoint’i ile aynı olacak şekilde ‘/connect/token’ değeriyle eşleştirdiğimize dikkatinizi çekerim. Bu endpoint üzerinden gelen token taleplerinde, request’in body’sinde bulunacak olan client-id ve client-secret gibi değerler doğrulanacak ve gerekli operasyonlar gerçekleştirilecektir. Client’ın kimlik bilgileri 14. satırdaki GetOpenIddictServerRequest fonksiyonu tarafından OpenIddict ile otomatik olarak doğrulanacak ve 15. satırda yapılan IsClientCredentialsGrantType kontrolü eşliğinde eğer ki doğrulama söz konusu olmazsa bu isteğe karşılık token üretilmeyecektir. Burada ekstradan şunu da söylemekte fayda vardır ki, bu action sadece client credential flow için değil tüm akışlar için kullanılabilir bir temele sahiptir.

    20. satırda, istemciden gelen client-id’ye karşılık client veritabanından sorgulanmakta ve ardından bu client’a özel üretilecek olan token içerisinde hangi claim’lerin olması gerekiyorsa 24. satırda oluşturulan ‘ClaimsIdentity’ nesnesine 28 ile 30. satır aralığında olduğu gibi eklenmektedir. Tabi burada unutulmaması gereken noktalardan birisi ‘Subject’ claim’inin zaruri olmasıdır. Ayrıca claim’lerin destination’ları belirtilerek hangi token’a eklenecekleri bildirilmelidir. Aksi taktirde üretilecek token’a bu claim’ler eklenmeyecektir.

    Özellikle 32. satırdaki JwtRegisteredClaimNames.Aud claim’ine dikkatinizi çekmek istiyorum. Bu claim, geliştirmiş olduğumuz bu Authorization Server’ın sorumlu olduğu resource server’larımızın(ya da bir başka deyişle API Resource’ların) veya API’ların, bu Authorization Server tarafından üretilen token’ı doğrulayabilmesi için kullanacağı ‘Audience’ verisiyle eşdeğer olacaktır. Onun için üretilecek token içerisinde claim olarak tanımlanmasına özen gösterilmelidir.

    Ve son olarak 38. satırda SignIn metodu ile yapılan return neticesinde OpenIddict kütüphanesinin devreye girmesi ve access token ile identity token değerlerinin üretiminin gerçekleştirmesi sağlanmakta ve bu değerler request yapan istemciye gönderilmektedir.

  • Adım 7 | (Token Talebinde Bulunma – Küçük Bir Test Edelim)
    Evet, artık yaptığımız bu inşayı bir token talebinde bulunarak test edebiliriz. Bunun için 5. adımda oluşturduğumuz client bilgilerinden aşağıdaki gibi istifade edeceğiz.
    OpenIddict #2 – Client Credentials FlowGörüldüğü üzere Postman üzerinden client bilgileri eşliğinde yapılan istek neticesinde Authorization Server’dan bir token elde etmiş bulunuyoruz. Bundan sonra artık bu token’ı kullanarak API Resource’lara isteklerimizi gönderiyor olacağız. Tabi bundan önce üretilen bu token değerini çözümleyerek içerisindeki bilgileri inceleyelim.
    OpenIddict #2 – Client Credentials FlowYukarıdaki ekran görüntüsünde görüldüğü gibi jwt.io üzerinden token değerini çözümlemiş bulunmaktayız. (Ha eğer ki sizde çözümleme başarılı değilse özellikle 2. adımdaki 35. satıra tekrar odaklanmanızı tavsiye ediyorum.) Payload içerisine göz atarsak eğer 6. adımda token içerisine koyulan ‘sub’, ‘name’, ‘ozel-claim’ vs. gibi tüm claim’leri görmekteyiz. Ayrıca ‘Audience’e karşılık gelen ‘aud’ bilgiside token içerisinde gelmektedir.

    Şimdi sırada API Resource’lara Authorization Server’dan aldığımız token ile istekte bulunmak var. Tabi bunun için öncelikle bir kaç örnek API Resource oluşturmamız gerekmektedir.

  • Adım 8 | (API Resource Oluşturma ve Yapılandırma)
    OpenIddict ile geliştirdiğimiz client credentials flow örneğini test edebilmek için birkaç API Resource’a ihtiyacımız olacaktır. Tabi ki de bizler bu içeriğimizde bir tanesinin nasıl yapılandırılacağını ele alıyor olacağız. Sonrasında ise sizler istediğiniz kadar API Resource’u oluşturabilirsiniz 🙂
    OpenIddict #2 – Client Credentials Flow
    Örnek API Resource’u aynı solution içerisinde adı opsiyonel olacak şekilde(ben API1 adını veriyorum) oluşturalım.

    Bu API’ı Authorization Server’dan yetki alacak şekilde yapılandırabilmek için ‘Program.cs’ dosyasını açalım ve aşağıdaki konfigürasyonları sağlayalım.

    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using System.Security.Claims;
    
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddControllers();
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.Authority = builder.Configuration["AuthenticationSettings:Authority"];
            //Token'daki 'JwtRegisteredClaimNames.Aud' karşılık verilen değerin ta kendisi...
            options.Audience = builder.Configuration["AuthenticationSettings:Audience"];
            options.RequireHttpsMetadata = false;
    
            //Gelen token'da 'scope' claim'i içerisinde kümülatif olarak yan yana yazılmış
            //yetkileri ayırarak tek tek scope claim'i olarak tekrardan ayarlayabilmek için
            //token'ın doğrulanması ardından 'OnTokenValidated' event'inde aşağıdaki çalışmayı
            //gerçekleştirmemiz gerekmektedir.
            options.Events = new()
            {
                OnTokenValidated = async context =>
                {
                    if (context.Principal?.Identity is ClaimsIdentity claimsIdentity)
                    {
                        Claim? scopeClaim = claimsIdentity.FindFirst("scope");
                        if (scopeClaim is not null)
                        {
                            claimsIdentity.RemoveClaim(scopeClaim);
                            claimsIdentity.AddClaims(scopeClaim.Value.Split(" ").Select(s => new Claim("scope", s)).ToList());
                        }
                    }
    
                    await Task.CompletedTask;
                }
            };
        });
    
    builder.Services.AddAuthorization(options =>
    {
        options.AddPolicy("APolicy", policy => policy.RequireClaim("scope", "read"));
        options.AddPolicy("BPolicy", policy => policy.RequireClaim("scope", "write"));
        options.AddPolicy("CPolicy", policy => policy.RequireClaim("scope", "read", "write"));
        options.AddPolicy("DPolicy", policy => policy.RequireClaim("ozel-claim", "ozel-claim-value"));
    });
    
    var app = builder.Build();
    
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    
    app.UseHttpsRedirection();
    
    app.UseAuthentication();
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    

    Yukarıdaki çalışmaya göz atarsanız eğer; 10 ile 38. satır aralığında jwt yapılandırması gerçekleştirilirken, 40 ile 46. satır aralığında ise uygulamada belli başlı yetkilendirmeleri örneklendirebilmek için politikalar oluşturulmaktadır. Burada özellikle dikkat edilmesi gereken husus 15. satırdaki ‘Audience’ ayarıdır. Bu token içerisindeki değerle birebir aynı şekilde olmalıdır. Aksi taktirde token doğrulanmayacak ve haliyle yetkilendirme başarısız olacaktır. Aynı durum ‘Authority’ içinde geçerlidir. ‘Authority’, token’ı üreten Authorization Server’ın adresiyle birebir aynı olmalıdır. Ayrıca 24 ile 37. satır aralığında token’dan gelen bütünleşik ‘scope’ claim’ini ayrı ayrı claim olarak ayarlamakta ve ‘ClaimsIdentity’e eklemekteyiz. Böylece yetkilendirme sürecinde ilgili token’ın hangi izinlere sahip olduğunu rahatlıkla ayırt edebilmekteyiz.

    Yetkilendirme yapılandırmasını inşa ettikten sonra 59 ve 60. satırda olduğu gibi UseAuthentication ve UseAuthorization middleware’lerini sırasıyla çağırmayı unutmayınız.

    41 ile 47. satır aralığında ise oluşturulan politikalar ‘scope’ ve ‘ozel-claim’e bağlı bir şekilde yetki kontrolleri gerçekleştirmektedirler. Böylece bu politikaları aşağıdaki gibi API controller’larında kullanabilir ve erişimleri yönetebiliriz.

        [Route("api/[controller]")]
        [ApiController]
        public class ValuesController : ControllerBase
        {
            [HttpGet("[action]")]
            [Authorize("APolicy")]
            public IActionResult A()
            {
                return Ok();
            }
    
            [HttpGet("[action]")]
            [Authorize("BPolicy")]
            public IActionResult B()
            {
                return Ok();
            }
    
            [HttpGet("[action]")]
            [Authorize("CPolicy")]
            public IActionResult C()
            {
                return Ok();
            }
    
            [HttpGet("[action]")]
            [Authorize("DPolicy")]
            public IActionResult D()
            {
                return Ok();
            }
        }
    
  • Adım 9 | (Nihai Test)
    Artık herşey hazır. Authorization Server eşliğinde bir tane numunelik Resource API’ın geliştirmesini tamamlamış olduk. Şimdi postman üzerinden client credentials flow ile token’ımızı talep edelim ve bu token ile Resource API’daki yukarıda oluşturduğumuz endpoint’lere tek tek erişim göstermeye çalışalım.
    OpenIddict #2 – Client Credentials FlowVee tebrikler 🙂 görüldüğü üzere uygulamalarımızın kusursuz çalıştığını ve client credentials flow’un OpenIddict ile başarıyla uygulandığını görmüş olduk.

Bir sonraki içeriğimizde Single Page Application(SPA) uygulamaları için önerilen Authorization Code Flow’u inceliyor olacağız.

O halde şimdilik görüşmek üzere…
İlgilenenlerin faydalanması dileğiyle…
İyi çalışmalar…

Not : Örnek uygulamanın kaynak kodlarına aşağıdaki github adresinden erişebilirsiniz.
https://github.com/gncyyldz/OpenIddict-Authorization-Server-Example

Bunlar da hoşunuza gidebilir...

1 Cevap

  1. Gençay dedi ki:

    Okurlarımın dikkatine;

    6. adımda token endpoint’inin tasarlanması için sunduğumuz aşağıdaki kodun;

    public class AuthorizationController : Controller
    {
        readonly IOpenIddictApplicationManager _applicationManager;
     
        public AuthorizationController(IOpenIddictApplicationManager applicationManager)
        {
            _applicationManager = applicationManager;
        }
     
        //Bu action'ın endpoint'ini token endpoint ile aynı şekilde ayarlıyoruz.
        [HttpPost("~/connect/token")]
        public async Task<IActionResult> Exchange()
        {
            var request = HttpContext.GetOpenIddictServerRequest();
            if (request?.IsClientCredentialsGrantType() is not null)
            {
                //Client credentials OpenIddict tarafından otomatik olarak doğrulanır.
                //Eğer ki gelen request'in body'sindeki client_id veya client_secret bilgileri geçersizse, burası tetiklenmeyecektir.
     
                var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
                if (application is null)
                    throw new InvalidOperationException("This clientId was not found");
     
                var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
     
                //Token'a claim'leri ekleyelim. Subject'in eklenmesi zorunludur.
                //Destination'lar ile claim'lerin hangi token'a ekleneceğini belirtiyoruz. AccessToken mı? Identity Token mı? Yoksa her ikisi de mi?
                identity.AddClaim(Claims.Subject, (await _applicationManager.GetClientIdAsync(application) ?? throw new InvalidOperationException()), Destinations.AccessToken, Destinations.IdentityToken);
                identity.AddClaim(Claims.Name, (await _applicationManager.GetDisplayNameAsync(application) ?? throw new InvalidOperationException()), Destinations.AccessToken, Destinations.IdentityToken);
                identity.AddClaim("ozel-claim", "ozel-claim-value", Destinations.AccessToken, Destinations.IdentityToken);
     
                identity.AddClaim(JwtRegisteredClaimNames.Aud, "Example-OpenIddict", Destinations.AccessToken, Destinations.IdentityToken);
     
                var claimsPrincipal = new ClaimsPrincipal(identity);
                claimsPrincipal.SetScopes(request.GetScopes());
     
                //SignIn return etmek, biryandan OpenIddict'ten uygun access/identity token talebinde bulunmaktır.
                return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            }
            throw new NotImplementedException("The specified grant type is not implemented.");
        }
    }
    

    28 ile 30. satır aralığındaki Destinations.AccessToken ve Destinations.IdentityToken tanımlamaları OpenIddict’in son sürümünde hata verecektir. Bu tanımları gerçekleştirebilmek için kodun aşağıdaki gibi düzeltilmesi gerekmektedir;

        public class AuthorizationController : Controller
        {
            readonly IOpenIddictApplicationManager _applicationManager;
    
            public AuthorizationController(IOpenIddictApplicationManager applicationManager)
            {
                _applicationManager = applicationManager;
            }
    
            //Bu action'ın endpoint'ini token endpoint ile aynı şekilde ayarlıyoruz.
            [HttpPost("~/connect/token")]
            public async Task<IActionResult> Exchange()
            {
                var request = HttpContext.GetOpenIddictServerRequest();
                if (request?.IsClientCredentialsGrantType() is not null)
                {
                    //Client credentials OpenIddict tarafından otomatik olarak doğrulanır.
                    //Eğer ki gelen request'in body'sindeki client_id veya client_secret bilgileri geçersizse, burası tetiklenmeyecektir.
    
                    var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
                    if (application is null)
                        throw new InvalidOperationException("This clientId was not found");
    
                    var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    
                    //Token'a claim'leri ekleyelim. Subject'in eklenmesi zorunludur.
                    //Destination'lar ile claim'lerin hangi token'a ekleneceğini belirtiyoruz. AccessToken mı? Identity Token mı? Yoksa her ikisi de mi?
    
                    identity.AddClaim(Claims.Subject, (await _applicationManager.GetClientIdAsync(application) ?? throw new InvalidOperationException()));
                    identity.AddClaim(Claims.Name, (await _applicationManager.GetDisplayNameAsync(application) ?? throw new InvalidOperationException()));
                    identity.AddClaim("ozel-claim", "ozel-claim-value");
    
                    identity.AddClaim(JwtRegisteredClaimNames.Aud, "Example-OpenIddict");
    
                    var claimsPrincipal = new ClaimsPrincipal(identity);
    
                    foreach (var claim in claimsPrincipal.Claims)
                        claim.SetDestinations(Destinations.AccessToken, Destinations.IdentityToken);
    
                    claimsPrincipal.SetScopes(request.GetScopes());
    
                    //SignIn return etmek, biryandan OpenIddict'ten uygun access/identity token talebinde bulunmaktır.
                    return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
                }
                throw new NotImplementedException("The specified grant type is not implemented.");
            }
        }
    

    Dikkat ederseniz 37 ve 38. satırlarda claimsPrincipal içerisindeki ‘Claim’lerde foreach ile dönülmekte ve tüm claim’lere SetDestinations fonksiyonu ile ilgili destinations’lar tanımlanmaktadır.

    Çalışırken olası hatayla karşılaşacakların bilgilenmesi ve hepinizin faydalanması dileğiyle…

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir