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

OpenIddict #3 – Authorization Code Flow

Merhaba,

Bu içeriğimizde OpenIddict kütüphanesi ile iki aşamalı doğrulama gerçekleştirmemizi gereki kılan Authorization Code Flow‘un nasıl uygulanabileceğini ele alıyor olacağız. Hadi buyurun başlayalım.

Authorization Code Nedir?

Tabi öncelikle Authorization Code’un ne olduğunu ele alarak başlamakta fayda olduğu kanaatindeyim. Bunun için zamanında Authorization Code Grant(Flow) başlığındaki makalede Authorization Code’u IdentityServer4 kütüphanesi eşliğinde tam teferruatlı klavyeye almış bulunmaktayım. Öncelikle ilgili içeriği okuyarak soluklanmanızı ve ardından buradan devam etmenizi öneririm.

Authorization Code; client kullanıcısının Authorization Server üzerinden authorize olması esnasında, client tarafından öğrenilmek ve elde edilmek istenen bilgilerin kullanıcı tarafından onaylanarak verilmesi neticesinde üretilen bir kodu temsil etmektedir. Kullanıcı için kritik olan bu kod, authorization server tarafından client’a verildiği taktirde client bu kod eşliğinde authorization server’dan ilgili kullanıcıya ait yetkilerde access token ve refresh token’ı elde edebilecektir.

Konuyu daha iyi anlayabilmek için aşağıdaki görseli inceleyebilirsiniz.OpenIddict #3 - Authorization Code Flow
Kullanıcı, client tarafından yetkilerine sahip olunabilmesi için client identifier, username ve password bilgileri eşliğinde authorization server’a yönlendirilir. Kullanıcı, client’ın belli başlı bilgilerine olan erişim talebini bu bilgilerle onaylar veya reddeder. Onay verildiği taktirde authorization code üretilir. Neticede tekrar kullanıcı client’a geri yönlendirilir.

Böylece authorization code’a sahip olan client, bu kodu kullanarak authorization server’dan access token talebinde bulunur. Authorization server ise client’tan gelen bu talep neticesinde hem authorization code’u hem de client credential bilgilerini doğrular ve kullanıcı yetkilerine sahip olan bir access token üretip client’a döndürür. Ve nihai olarak bu access token ile client artık yetkilendirilmiş olur ve artık istediği resource server’lara(API resource’lara) isteklerde bulunabilir.

İşte tüm bu süreçte client’ı yetkilendirebilmek için önce authorization code’a ardından da access token’a ihtiyaç duyulduğu için iki aşamalı doğrulama söz konusudur.

Bu girizgâh sonra artık OpenIddict ile Authorization Code Flow’un yapılandırmasına geçebiliriz.

OpendIddict ile Authorization Code Flow Yapılandırması

Not : Buradaki yapılandırma sürecinde kullanılan proje temelleri yazı serisi boyunca oluşturulup geliştirilen şu adresteki github reposundan çekilmiştir.
İlk olarak authorization code flow’u etkinleştirebilmek için aşağıdaki konfigürasyonların yapılması gerekmektedir;

//OpenIddict servisini uygulamaya ekliyoruz.
builder.Services.AddOpenIddict()
    .AddCore(options =>
    {
        .
        .
        .
    })
    .AddServer(options =>
    {
        options.SetTokenEndpointUris("/connect/token")
               //Authorization Code talebinde bulunulacak endpoint'i set ediyoruz.
               .SetAuthorizationEndpointUris("/connect/authorize");
        
        options.AllowClientCredentialsFlow()
               //Authorization Code Flow'u etkileştiriyoruz.
               .AllowAuthorizationCodeFlow()
               .RequireProofKeyForCodeExchange();
        .
        .
        .
        options.UseAspNetCore()
               .EnableTokenEndpointPassthrough()
               //EnableAuthorizationEndpointPassthrough: OpenID Connect request'lerinin Authorization Endpoint için aktifleştirilmesini sağlar.
               .EnableAuthorizationEndpointPassthrough();
       .
       .
       .
    });

Yukarıdaki konfigürasyonel kodları incelersek eğer; 13. satırda SetAuthorizationEndpointUris metodu ile authorization code talebi için endpoint tanımlamasında bulunuyoruz. 17. satırda ise AllowAuthorizationCodeFlow metodu ile authorization code flow’u etkinleştiriyor ve hemen ardından 18. satırda RequireProofKeyForCodeExchange metodu ile de tüm bu client’ların PKCE kullanması gerektiğini ifade ediyoruz. 25. satırda ise EnableAuthorizationEndpointPassthrough metodu ile gelen request’leri authorization endpoint’i ile karşılıyoruz.

Authorization Endpoint’inin Yapılandırılması

Şimdi, authorization code talebinde bulunulabilmesi için ‘AuthorizationController’ içerisinde aşağıdaki authorization endpoint görevini görecek olan ‘Authorize’ action metodunu inşa edelim.

    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"), Produces("application/json")]
        public async Task<IActionResult> Exchange()
        {
            .
            .
            .
        }

        [HttpGet("~/connect/authorize"), HttpPost("~/connect/authorize")]
        public async Task<IActionResult> Authorize(string accept, string deny)
        {
            var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

            //Cookie'de authentication için tutulan veriden kullanıcı bilgisini alıyoruz.
            var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            //Eğer kullanıcı bilgisi yoksa kullanıcıyı login sayfasına yönlendiriyoruz.
            if (!result.Succeeded)
                return Challenge(
                    authenticationSchemes: CookieAuthenticationDefaults.AuthenticationScheme,
                    properties: new AuthenticationProperties()
                    {
                        RedirectUri = $"{Request.PathBase}{Request.Path}{(QueryString.Create(Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList()))}"
                    });

            var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            identity.AddClaim(OpenIddictConstants.Claims.Subject, result.Principal.Identity.Name, Destinations.AccessToken);
            identity.AddClaim(JwtRegisteredClaimNames.Aud, "Example-OpenIddict", Destinations.AccessToken, Destinations.IdentityToken);
            identity.AddClaim("ornek-claim", "ornek claim value", Destinations.AccessToken);

            var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
            if (HttpContext.Request.Method == "GET")
                return View(new AuthorizeVM
                {
                    ApplicationName = await _applicationManager.GetLocalizedDisplayNameAsync(application),
                    Scopes = request.Scope
                });
            else if (!string.IsNullOrEmpty(accept))
            {
                var claimsPrincipal = new ClaimsPrincipal(identity);
                claimsPrincipal.SetScopes(request.GetScopes());

                return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            }

            return Forbid(authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                properties: new AuthenticationProperties(new Dictionary<string, string>
                {
                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidScope
                }));
        }
    }

İlgili metodun içeriğini incelerseniz eğer; Client Credentials Flow‘dan farklı olarak authorization code, onay için kullanıcı bilgisine odaklanmaktadır. Her ne kadar client authorization server’dan kullanıcı bilgilerini talep ediyor olsa da bizler kullanıcının önceden oturum açıp açmadığını 25. satırda denetliyor, gerekirse 27 ile 33. satır aralığında olduğu gibi login sayfasına yönlendiriyoruz.

Kullanıcı kimliği doğrulanmışsa eğer client’a kullanıcıya dair verilecek bilgiler 35 ile 38. satır aralığında claim olarak hazırlanmaktadır. 41 ile 53. satır aralığında ise client’ın, authorization server’dan kullanıcıya dair isteyeceği yetkilerin kullanıcı taraflı onayını sunmakta ve bu onayın verilip verilmediği kontrol edilmektedir. 42 ile 46. satır aralığında onay sayfasına yönlendirme gerçekleştirilmekte, 48 ile 53. satır aralığında ise onay verildiyse SignIn fonksiyonunu kullanarak authorization code’a karşılık access token döndürebilmesi için OpenIddict middleware’i tetiklemektedir. Ve son olarak 55 ile 59. satır aralığında ise onay verilmediği taktirde Forbid metodu ile yetkilendirmeye engel olunmaktadır.

Burada onay sayfası tarafımızca aşağıdaki gibi tasarlanmıştır.

@using Microsoft.Extensions.Primitives
@using OpenIddict.AuthorizationServer.ViewModels
@model AuthorizeVM
<div class="jumbotron">
    <h1>Yetki Onaylama</h1>

    <p class="lead text-left"><strong>@Model.ApplicationName</strong> uygulamasına erişim yetkisi vermek istiyor musunuz? (Beklenen yetkiler: @Model.Scopes)</p>

    <form asp-controller="Authorization" asp-action="Authorize" method="post">
        @foreach (var parameter in Context.Request.HasFormContentType ?
        (IEnumerable<KeyValuePair<string, StringValues>>)Context.Request.Form : Context.Request.Query)
        {
            <input type="hidden" name="@parameter.Key" value="@parameter.Value" />
        }

        <input class="btn btn-lg btn-success" name="accept" type="submit" value="Yes" />
        <input class="btn btn-lg btn-danger" name="deny" type="submit" value="No" />
    </form>
</div>

Ve kullanılan viewmodel’ın içeriğini de paylaşmamız gerekirse eğer;

    public class AuthorizeVM
    {
        public string ApplicationName { get; set; }
        public string Scopes { get; set; }
        public string Button { get; set; }
    }

Token Endpoint’inin Yapılandırılması

Artık authorization code flow’u desteklendiğimiz için token endpoint’i ona göre yapılandırmamız 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"), Produces("application/json")]
        public async Task<IActionResult> Exchange()
        {
            var request = HttpContext.GetOpenIddictServerRequest();


            if (request?.IsAuthorizationCodeGrantType() is not null)
            {
                //Authorization Code'da store edilen request sorumlusunu elde ediyoruz.
                var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;

                //Principal'ı yani kullanıcıyı doğrula
                //if ((await _userManager.GetUserAsnyc(principal)) != null)
                //{

                //}

                return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

            }
            else if (request?.IsClientCredentialsGrantType() is not null)
            {
                .
                .
                return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            }
            throw new NotImplementedException("The specified grant type is not implemented.");
        }

        [HttpGet("~/connect/authorize"), HttpPost("~/connect/authorize")]
        public async Task<IActionResult> Authorize(string accept, string deny)
        {
            .
            .
            .
        }
    }

Yukarıdaki kod bloğunu incelerseniz eğer 17 ile 30. satır aralığını ekstradan eklemiş ve IsAuthorizationCodeGrantType fonksiyonu ile ilgili akışa göre bir çalışma gerçekleştirmiş bulunuyoruz.

Client’a Redirect Urls Bilgisinin Verilmesi

Authorization server’dan, authorization code için talepte bulunan client geri dönüş url’i olarak hangi adreste karşılama yapacağını bildirmek mecburiyetindedir. Haliyle bizler postman uygulamasını test amaçlı kullanacağımızdan dolayı postman client görevi görecektir ve bundan dolayı authorization code flow ile yapılan talepler neticesinde authorization server’dan postman’e geri dönüş yapılabilmesi için postman’in bizlere sunmuş olduğu şu:https://oauth.pstmn.io/v1/callback adresi kullanılıyor olacaktır.

Bunun için client’ların tanımlandığı veya bir başka deyişle kayıt edildiği ‘ClientsController’ sınıfının içerisindeki ‘CreateClient’ metodunda aşağıdaki gibi çalışmanın yapılması gerekmektedir.

    public class ClientsController : Controller
    {
        readonly IOpenIddictApplicationManager _openIddictApplicationManager;

        public ClientsController(IOpenIddictApplicationManager openIddictApplicationManager)
        {
            _openIddictApplicationManager = openIddictApplicationManager;
        }

        .
        .
        .

        [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,
                    RedirectUris = { new("https://oauth.pstmn.io/v1/callback") },
                    Permissions =
                    {
                        OpenIddictConstants.Permissions.Endpoints.Token,
                        OpenIddictConstants.Permissions.Endpoints.Authorization,

                        OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
                        OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,

                        OpenIddictConstants.Permissions.Prefixes.Scope + "read",
                        OpenIddictConstants.Permissions.Prefixes.Scope + "write",

                        OpenIddictConstants.Permissions.ResponseTypes.Code,
                    }
                });
                ViewBag.Message = "Client başarıyla oluşturulmuştur.";
                return View();
            }
            ViewBag.Message = "Client zaten mevcuttur.";
            return View();
        }
    }

Ya da client olarak postman yerine farklı bir uygulama kullanılacaksa eğer yukarıdaki kod bloğunun 25. satırı o uygulamanın redirect url’ine göre ayarlanması gerekmektedir.

        .
        .
        RedirectUris = { new("https://filanca.com/...") },

Ek olarak client’ın izinleri olarak da ‘Permissions’ property’sine ‘Authorization’, ‘AuthorizationCode’ ve ‘Code’ yetkilerinin de verildiğine dikkatinizi çekerim.

Ayrıca burada isterseniz redirect url’i yine client’tan talep edebilirsiniz. Tabi bu sefer bahsedilen client eklenecek olan değil, bu ekleme işlemini yapacak olan client’tır 🙂

Client’ların redirect url’lerini bu şekilde belirledikten sonra yeni gelecek olan client kayıtlarında verilerin aşağıdaki gibi veritabanına yansıtıldığını gözlemliyor olacağız.
OpenIddict #3 - Authorization Code Flow

Postman Üzerinden Test Edilmesi

Artık yaptığımız çalışmayı test edip, gözlemleyebiliriz. Bunun için ilk olarak postman’in ‘OAuth 2.0’ türü üzerinden bir token talebinde bulunacak ardından kendi geliştireceğimiz custom bir client üzerinden de testimizi gerçekleştiriyor olacağız.
OpenIddict #3 - Authorization Code FlowOpenIddict #3 - Authorization Code FlowYukarıdaki ekran görüntüsünde görüldüğü üzere gerekli bilgileri postman arayüzüne işledikten sonra aşağıdaki ‘Get New Access Token’ butonuna tıkladığımızda yandaki gibi onay sayfasına yönlendirildiğimizi göreceğiz. Burada ‘Yes’ ya da ‘No’ butonları işlevlerine göre authorization code’un üretilmesi sürecini ya başlatacaktır ya da başlamadan iptal edecektir. ‘Yes’ butonuna tıklandığı vakit önceki satırlarda geliştirdiğimiz ‘AuthorizationController’ içerisindeki ‘Authorize’ action’ına bir POST atılacak ve burada authorization code ile gerekli access token’ın üretilebilmesi için SignIn metodu eşliğinde OpenIddict middleware’i devreye sokulacaktır. Ardından bu işlemden sonra authorization code üretilecek ve ilgili client’ın redirect url’ine ‘code’ query string’i eşliğinde gönderilecektir.OpenIddict #3 - Authorization Code Flowİşte postman’de ilgili redirect url’in ekran görüntüsü yukarıdaki gibidir ve daha da önemlisi üretilmiş olan authorization code ‘code’ query string parametresinde olduğu gibi client tarafından elde edilmektedir.

Bu sayfa açıldığı taktirde elde edilen authorization code ile ‘AuthorizationController’ içerisindeki ‘Exchange’ action’ına authorization code grant türünde istek gönderilmekte ve ilgili authorization code’a nazaran access token üretilerek geri döndürülmektedir.
OpenIddict #3 - Authorization Code FlowBu aşamadan sonra ilgili access token’ı kullanarak Resource Server’lara(ya da API Server’lara) yapılan tüm istekler başarılı şekilde sonuçlanacaktırlar.OpenIddict #3 - Authorization Code Flow

Access Token’ın jwt.io‘da Decode Edilmesi

Üretilen access token değerini decode edip incelememiz gerekirse eğer bunun için jwt.io‘dan istifade edebiliriz.
OpenIddict #3 - Authorization Code Flow

Custom Geliştirilen Asp.NET Core MVC Temelli Client Üzerinden Test Edilmesi

Şimdide yaptığımız çalışmayı custom geliştireceğimiz uygulama üzerinden test edelim. Bunun için ‘Client’ adında bir Asp.NET Core MVC uygulaması oluşturarak içerisine aşağıdaki kütüphaneleri yükleyerek başlayabiliriz.

Bu kütüphanelerle Asp.NET Core uygulamasını client olarak tasarlayabilecek ve gerekli konfigürasyonları sağlayabileceğiz.

Temel kütüphaneleri yükledikten sonra ‘Program.cs’ dosyasında aşağıdaki yapılandırmalarda bulunalım;

using Client.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using OpenIddict.Client;
using static OpenIddict.Abstractions.OpenIddictConstants;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("SQLServer"));
    options.UseOpenIddict();
});

builder.Services.AddAuthentication(options => options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
    {
        options.LoginPath = "/login";
        options.ExpireTimeSpan = TimeSpan.FromMinutes(15);
    });

builder.Services.AddOpenIddict()
    .AddCore(options =>
    {
        options.UseEntityFrameworkCore()
        .UseDbContext<ApplicationDbContext>();

        //Burada MongoDB kullanmak isterseniz aşağıdaki yapılandırma kodlarından istifade edebilirsiniz.
        // options.UseMongoDb()
        //        .UseDatabase(new MongoClient().GetDatabase("openiddict"));
    })
    //OpenIddict client componentinin yapılandırması.
    .AddClient(options =>
    {
        options.SetRedirectionEndpointUris("/callback/login/local");
        options.SetPostLogoutRedirectionEndpointUris("/callback/logout/local");

        options.AddDevelopmentEncryptionCertificate()
               .AddDevelopmentSigningCertificate();

        options.UseAspNetCore()
                      .EnableStatusCodePagesIntegration()
                      .EnableRedirectionEndpointPassthrough()
                      .EnablePostLogoutRedirectionEndpointPassthrough();

        options.UseSystemNetHttp();

        options.AddRegistration(new OpenIddictClientRegistration
        {
            Issuer = new Uri("https://localhost:7047", UriKind.Absolute),

            ClientId = "my-client",
            ClientSecret = "my-client-secret",
            Scopes = { "read", "write" },

            RedirectUri = new Uri("https://localhost:7226/callback/login/local", UriKind.Absolute),
            PostLogoutRedirectUri = new Uri("https://localhost:7226/callback/logout/local", UriKind.Absolute),
        });
    });

builder.Services.AddHttpClient();

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();

Burada özellikle 37 ile 63. satır aralığına göz atarsanız eğer OpenIddict için gerekli client yapılandırmaları sağlanmaktadır. SetRedirectionEndpointUris ve SetPostLogoutRedirectionEndpointUris fonksiyonları ile authorization server’a yapılan authorization code yahut logout talepleri için uygulamadaki yönlendirme adresleri bildirilmektedir. Özellikle 52 ile 62. satır aralığında ise AddRegistration fonksiyonu eşliğinde client bilgileri tanımlanmaktadır. Tabi buradaki client bilgilerini esasında authorization server’a gidip tanımlamanız gerektiğini hatırlatmakta fayda görmekteyim.

Bu tanımlamaları yaptıktan sonra oluşturduğumuz bu client uygulamasında ‘AuthenticationController.cs’ isminde bir controller sınıfı oluşturalım ve içeriğini aşağıdaki gibi dolduralım.

    public class AuthenticationController : Controller
    {
        [HttpGet("~/login")]
        public IActionResult LogIn(string returnUrl)
        {
            var properties = new AuthenticationProperties(new Dictionary<string, string?>
            {
                [OpenIddictClientAspNetCoreConstants.Properties.Issuer] = "https://localhost:7047"
            });
            properties.RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/";

            //Challenge metodu ile OpenIddict middleware'ı sayesinde ilgili Issuer'a karşılık gelen client bilgilerini authorization server'a yönlendiriyoruz.
            return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
        }

        [HttpPost("~/logout"), ValidateAntiForgeryToken]
        public async Task<ActionResult> LogOut(string returnUrl)
        {
            //Elde bulunan authentication cookie bilgilerini elde ediyoruz. Eğer yoksa zaten kullanıcının henüz oturum açmadığını anlıyoruz.
            var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            if (result is not { Succeeded: true })
                return Redirect(Url.IsLocalUrl(returnUrl) ? returnUrl : "/");

            //SignOut yaparak mevcut authentication cookie bilgilerini temizliyoruz.
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

            var properties = new AuthenticationProperties(new Dictionary<string, string?>
            {
                [OpenIddictClientAspNetCoreConstants.Properties.Issuer] = "https://localhost:7047",
                [OpenIddictClientAspNetCoreConstants.Properties.IdentityTokenHint] = result.Properties.GetTokenValue(OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken)
            });
            properties.RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/";

            return SignOut(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
        }

        [HttpGet("~/callback/login/{provider}"), HttpPost("~/callback/login/{provider}"), IgnoreAntiforgeryToken]
        public async Task<ActionResult> LogInCallback()
        {
            // OpenIddict tarafından doğrulanan yetkilendirme verilerini elde ediyoruz.
            var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);

            if (result.Principal.Identity is not ClaimsIdentity { IsAuthenticated: true })
            {
                throw new InvalidOperationException("The external authorization data cannot be used for authentication.");
            }

            var claims = new List<Claim>(result.Principal.Claims
                .Select(claim => claim switch
                {
                    { Type: Claims.Subject }
                        => new Claim(ClaimTypes.NameIdentifier, claim.Value, claim.ValueType, claim.Issuer),
                    { Type: Claims.Name }
                        => new Claim(ClaimTypes.Name, claim.Value, claim.ValueType, claim.Issuer),
                    _ => claim
                })
                .Where(claim => claim switch
                {
                    { Type: ClaimTypes.NameIdentifier or ClaimTypes.Name } => true,
                    _ => false
                }));

            var identity = new ClaimsIdentity(claims,
                authenticationType: CookieAuthenticationDefaults.AuthenticationScheme,
                nameType: ClaimTypes.Name,
                roleType: ClaimTypes.Role);

            var properties = new AuthenticationProperties(result.Properties.Items);

            // Gerekirse authorization server tarafından döndürülen tokenlar authentication cookie'de de saklanabilir.
            properties.StoreTokens(result.Properties.GetTokens().Where(token => token switch
            {
                {
                    Name: OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken or
                          OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken or
                          OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken
                } => true,
                _ => false
            }));

            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), properties);

            return Redirect(properties.RedirectUri);
        }

        [HttpGet("~/callback/logout/{provider}"), HttpPost("~/callback/logout/{provider}"), IgnoreAntiforgeryToken]
        public async Task<ActionResult> LogOutCallback()
        {
            var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
            return Redirect(result!.Properties!.RedirectUri);
        }
    }

Yukarıdaki kod bloğuna göz atarsanız eğer; 4 ile 14. satır aralığındaki LogIn metodu içerisinde kullanıcı tarafından onay istenecek şekilde authorization server üzerinden login işlemleri gerçekleştirilmektedir.

17 ile 35. satır aralığında ise LogOut action’ı içerisinde logout operasyonları gerçekleştirilmektedir. Tabi bu operasyonların sağlıklı bir şekilde gerçekleşebilmesi için authorization server’da bir kaç konfigürasyona ihtiyaç olacaktır. Bunlardan ilki client eklerken PostLogoutRedirectUris property’sine logout anında geri dönüş değerinin verilmesi, bir diğeri ise ‘Program.cs’de SetLogoutEndpointUris("/connect/logout") metodu ile logout endpoint url’inin set edilmesidir.

38 ile 84. satır aralığında ise login yapıldıktan sonra authorization server’dan gelecek olan authorization code’u karşılayıp gerekli signin işlemleri yürütülmektedir. Aynı şekilde 81 ile 91. satır aralığında da logout neticesinde authentication cookie’leri temizlenmektedir.

Bu işlemlerden sonra artık client tarafından edinilen kullanıcı yetkilerini deneyimleyebileceğimiz bir çalışma yapmamız yeterli olacaktır. Bunun için ‘HomeController’ içerisinde aşağıdaki gibi inşada bulunabiliriz.

    public class HomeController : Controller
    {
        readonly IHttpClientFactory _httpClientFactory;

        public HomeController(IHttpClientFactory httpClientFactory)
            => _httpClientFactory = httpClientFactory;

        public IActionResult Index()
            => View();

        [HttpPost]
        [Authorize]
        public async Task<IActionResult> Index(CancellationToken cancellationToken)
        {
            string? token = await HttpContext.GetTokenAsync(CookieAuthenticationDefaults.AuthenticationScheme, OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken);

            using HttpClient httpClient = _httpClientFactory.CreateClient();
            using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, "https://localhost:7056/api/values/a");
            httpRequestMessage.Headers.Authorization = new("Bearer", token);

            using HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(httpRequestMessage, cancellationToken);

            return View(model: await httpResponseMessage.Content.ReadAsStringAsync(cancellationToken));
        }
    }
@model string

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

<form asp-action="Index" asp-controller="Home">
    <button>İstek Yap</button>
</form>
<form asp-action="Logout" asp-controller="Authentication" method="post">
    <button class="btn btn-lg btn-danger" type="submit">Sign out</button>
</form>

<p>
    @Model
</p>

Görüldüğü üzere ‘Index’ action’ının post versiyonu authorize edilmiş vaziyettedir. Yani yetkilendirme beklemektedir. Uygulamayı ayağa kaldırıp, login olduktan sonra istek yap butonuna tıklandığı taktirde requestin API Resource’a isteğin başarıyla gidip geldiğini yani client’ın başarıyla kullanıcı izinleriyle yetkilendirildiğini gözlemliyor olacağız.

OpenIddict Kütüphanesi İle Authorization Code Süreçlerindeki Veritabanında Oluşan Hareketliliği İnceleyelim

Client uygulaması ile authorization server üzerinden yapılan her login talebinde ‘OpenIddictTokens’ tablosuna bir kayıt atılmaktadır.OpenIddict #3 - Authorization Code FlowAtılan kayıt bu isteğin ilk süreci olduğu için state_token type değerinde tutulmaktadır. Kullanıcı tarafından onay verilip authorization code elde edildiği taktirde ilgili tabloya bu token değerleri de işleniyor olacaktır.OpenIddict #3 - Authorization Code FlowVe dikkat ederseniz eğer ‘OpenIddictAuthorizations’ tablosunda da ilgili client’a dair hangi tarihler arasında bir authorization işlemi gerçekleştirildiği tutulmaktadır. Anlayacağınız, authorization server üzerinden yapılan tüm talep süreçlerinde ilgili tablolara kayıtların bu seyirde işlenmesi devam edecektir…

Evet, böylece OpenIddict kütüphanesi ile Authorization Code Flow’un nasıl uygulanabileceğini incelemiş olduk. Normalde custom uygulamaya örnek olarak console projesini de sunmak isterdim lakin içeriğin hacmini haddinden fazla şişirmemek için yorumlarda paylaşmak üzere makalemizi burada noktalıyorum.

Okuyarak eşlik ettiğiniz için teşekkür ederim.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İ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...

2 Cevaplar

  1. Nuh dedi ki:

    3-4 gundur OpenId – OAuth2 ve OpenIdDict uzerine arastirma yapiyorum.

    OpenId – Oauth2 mantığını anlamış olmama rağmen openiddic implemantasyonu kendi dökümanlarından çözemeyeceğim kadar karmaşık geldi bana.

    Repoyu çekip kodu inceleyeceğim.

    Altın gibi blog yazısı.

    Teşekkürler Gençay Yıldız.

Bir cevap yazın

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