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

IdentityServer4 Yazı Serisi #22 – Resource Owner Credential Grant Uygulaması

Merhaba,

IdentityServer4 Yazı Serisinin yirmiikinci makalesinde Resource Owner Credential Grant akış tipini kullanan uygulama geliştireceğiz.

Başlarken
İlk olarak bu akış türünün özelliğini hatırlayarak başlayalım. Resource Owner Credential Grant, kullanıcıyı yönlendirme yapmaksızın direkt olarak Auth Server üzerinden yetkilendiren bir akış türüdür. Bu akış türünü daha detaylı inceleyebilmek için yukarıda adresi verilen makaleyi okumanızı tavsiye ederim.

Auth Server Uygulaması
Örneklendirmeyi MVC client uygulaması tasarlayarak gerçekleştireceğiz. Tabi herşeyden önce bu uygulamaya karşılık Auth Server’da client tasarlayarak başlayalım;
(Not: Config.cs dosyası ve IdentityServer4 konfigürasyonları yazı serisi boyunca değerlendirildiği için bu içerikte ele alınmayacak, yapıldıkları varsayılacaktır.)

                .
                .
                .
                new Client{
                    ClientId = "ResourceOwnerClient",
                    ClientName = "Resource Owner Client",
                    ClientSecrets = { new Secret("resourceownerclient".Sha256()) },
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                    AllowedScopes =
                    {
                        "Garanti.Write", "Garanti.Read",
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Email,
                        "Roles" //Identity Resource
                    },
                    AccessTokenLifetime = 3 * 60,
                    AllowOfflineAccess = true,
                    RefreshTokenUsage = TokenUsage.OneTimeOnly,
                    RefreshTokenExpiration = TokenExpiration.Absolute,
                    AbsoluteRefreshTokenLifetime = (3 * 60) + 30,
                    RequirePkce = false
                }
                .
                .
                .

Yukarıdaki client tanımlamasında görüldüğü üzere 8. satırda olduğu gibi ‘AllowedGrantTypes’ özelliğine ‘GrantTypes.ResourceOwnerPassword’ değeri verilerek ilgili akış türüyle çalışılacağı bildirilmektedir. Geri kalan tüm özellikler ise önceden incelediğimiz konfigürasyonlara karşılık gelmektedir.

Akabinde ilgili uygulama üzerinden giriş yapabilecek bir user’a ihtiyacımız olacaktır. Bunun için aşağıdaki gibi bir test user tanımlaması yapmamız yeterlidir;

                new TestUser
                {
                     SubjectId = "test-user1",
                     Username = "test-user1",
                     Password = "12345",
                     Claims = new List<Claim>
                     {
                         new Claim("email", "test-user@identity.com"),
                         new Claim("role", "admin"),
                         new Claim("role", "moderator"),
                         new Claim("name", "hilmi")
                     }
                }

Evet, MVC uygulamamızın Resource Owner Credential akışı ile çalışması için Auth Server neredeyse hazır. Şimdi MVC client’ı üzerinden bu akış ile bir istek yapıldığı zaman o isteği karşılayacak ve gerekli username ve password bilgileri eşliğinde kimlik doğrulamayı gerçekleştirecek olan IResourceOwnerPasswordValidator katmanını tasarlamamız gerekmektedir.

IResourceOwnerPasswordValidator Interface’i İle Kimlik Bilgilerini Doğrulama
Yukarıda da bahsedildiği gibi Resource Owner Credential akışı ile kimlik doğrulamayı gerçekleştirebilmek için Auth Server’da IResourceOwnerPasswordValidator interface’inden türeyen bir sınıf tasarlamamız gerekmektedir.

    public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
    {
        public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
        {
            if (context.UserName == "test-user1" && context.Password == "12345")
                context.Result = new GrantValidationResult("test-user1", OidcConstants.AuthenticationMethods.Password);
        }
    }

Yukarıdaki sınıfı incelerseniz ilgili interface tarafından implemente edilen ‘ValidateAsync’ metodu içerisinde kimlik doğrulama işlemi gerçekleştirilmektedir. Tabi burada test user kullandığımızdan dolayı manuel bir karşılaştırma olsa da esasında kullanıcı bilgileri veritabanında tutulacağı için gerekli çalışmanın yapılması ve veritabanı üzerinden sorgulanması gerekmektedir. Velhasıl bir şekilde kullanıcı bilgileri doğrulandıktan sonra ilgili metodun ‘ResourceOwnerPasswordValidationContext’ türünden olan context isimli parametresinin ‘Result’ property’sine ‘GrantValidationResult’ türünden bir nesne set ediyor ve ilk parametresine kullanıcının subject id değerini, ikincisine ise kullanılan authentication metot türünü yani resource owner grant(nam-ı diğer password) değerini veriyoruz.

Ardından ilgili sınıfı Auth Server’ın ‘Startup.cs’ dosyasında ‘AddResourceOwnerValidator’ metodu eşliğinde IdentityServer servisine dahil ediyoruz.

            services.AddIdentityServer()
                    .
                    .
                    .
                    .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
                    .AddDeveloperSigningCredential();

MVC Uygulaması
Sıra client uygulamasını geliştirmeye gelmiştir. İlk olarak MVC client’ını geliştireceğimizden dolayı bir adet Asp.NET Core MVC uygulaması oluşturalım ve içerisine ‘HomeController’ı ekleyelim. Ardından ilgili controller içerisine ‘Login’ action metodunu oluşturalım ve .cshtml dosyasını aşağıdaki gibi tasarlayalım.

@model UserLogin

<form method="post">
    <div class="form-group">
        <label>Username</label>
        <input asp-for="Username" type="text" class="form-control">
    </div>
    <div class="form-group">
        <label>Password</label>
        <input type="password" asp-for="Password" class="form-control">
    </div>
    <button type="submit" class="btn btn-primary">Giriş Yap</button>
</form>

Bu, kullanıcı girişinin yapılacağı sayfadır. Username ve password bilgilerini taşıyan ‘UserLogin’ modeli ise aşağıdaki gibidir.

    public class UserLogin
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }

Post neticesinde verileri karşılayacak olan metoduda tasarlarsak eğer;

        public async Task Login(UserLogin userLogin)
        {
            HttpClient client = new HttpClient();
            DiscoveryDocumentResponse disco = await client.GetDiscoveryDocumentAsync("https://localhost:1000");
            PasswordTokenRequest password = new PasswordTokenRequest()
            {
                UserName = userLogin.Username,
                Password = userLogin.Password,
                ClientId = "ResourceOwnerClient",
                ClientSecret = "resourceownerclient",
                Address = disco.TokenEndpoint
            };
            TokenResponse token = await client.RequestPasswordTokenAsync(password);
            UserInfoRequest userInfoRequest = new UserInfoRequest
            {
                Token = token.AccessToken,
                Address = disco.UserInfoEndpoint
            };
            UserInfoResponse userInfo = await client.GetUserInfoAsync(userInfoRequest);
            ClaimsIdentity claimsIdentity = new ClaimsIdentity(userInfo.Claims, CookieAuthenticationDefaults.AuthenticationScheme, "name", "role");
            ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
            AuthenticationProperties properties = new AuthenticationProperties();
            properties.StoreTokens(new List<AuthenticationToken>
            {
              new AuthenticationToken
                                     {
                                         Name = OpenIdConnectParameterNames.IdToken,
                                         Value = token.IdentityToken
                                     },
              new AuthenticationToken
                                     {
                                         Name = OpenIdConnectParameterNames.AccessToken,
                                         Value = token.AccessToken
                                     },
              new AuthenticationToken
                                     {
                                         Name = OpenIdConnectParameterNames.RefreshToken,
                                         Value = token.RefreshToken
                                     },
              new AuthenticationToken
                                     {
                                         Name = OpenIdConnectParameterNames.ExpiresIn,
                                         Value = DateTime.UtcNow.AddSeconds(token.ExpiresIn).ToString("O")
                                     },
            });
            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, properties);
        }

şeklinde olacaktır. Yukarıdaki kod bloğunu izah edersek eğer;

  • 3 – 12. satır aralığı
    Klasik discovery endpoint‘e istek gönderilmekte ve token talebi için gerekli request nesnesi(PasswordTokenRequest) üretilmektedir.
  • 13. satır
    Auth Server’a ‘RequestPasswordTokenAsync’ fonksiyonu ile resource owner credential türünde istek yapılmakta ve token talep edilmektedir.
  • 14 – 19. satır aralığı
    Elde edilen token’da ki access token değeri ile kullanıcı bilgileri talep edilmektedir.
  • 20 – 21. satır aralığı
    Kullanıcı bilgileri tutacak olan ‘ClaimsPrincipal’ nesnesi oluşturulmaktadır. Burada dikkat edilmesi gereken husus 20. satırda üretilen new ClaimsIdentity(userInfo.Claims, CookieAuthenticationDefaults.AuthenticationScheme, "name", "role"); nesnesinin ilk parametresi dışında 2. , 3. ve 4. parametreleridir. 2. parametre, ilgili uygulamanın kullandığı cookie adını gerektirirken(aşağıdaki satırlarda göreceğiz), 3. parametre User.Identity.Name koduna karşılık kullanıcıdan gelen hangi claim ile eşleştirilmesi gerektiğini bildirmekte ve 4. parametre ise benzer şekilde kullanıcının authorize/rol yetkisinin yine kullanıcıdan gelen claim’lerden hangisiyle eşleşmesi gerektiğini belirlemektedir.

    Burada ilgili claim değerlerinin elde edilebilmesi için Auth Server’da aşağıdaki Identity Resource’lerin eklenmesi gerektiğini unutmayınız…

                    .
                    .
                    .
                    new IdentityResources.OpenId(),
                    new IdentityResources.Profile(),
                    new IdentityResources.Email(),
                    new IdentityResource
                    {
                        Name = "Roles",
                        Description = "Roles",
                        DisplayName = "Roles",
                        UserClaims =
                        {
                            "role"
                        }
                    }
                    .
                    .
                    .
    
  • 22 – 44 satır aralığı
    Burada giriş yapan kullanıcının elde ettiği authentication properties bilgilerini client uygulaması üzerinde güncellemekteyiz. Hatta hatırlarsanız IdentityServer4 mimarisi için refresh token konusunu ele alırken de birebir aynı işlemi gerçekleştirmiştik. Lakin ilgili makalede authentication property’leri
    (await HttpContext.AuthenticateAsync()).Properties; kodu ile elde ediyor, üzerine değişikliklerimizi gerçekleştiriyorduk. Halbuki burada direkt olarak ‘AuthenticationProperties’ türünden nesneyi elimizle manuel oluşturuyoruz.
  • 46. satır
    Giriş işlemini yukarıda üretilen değerler eşliğinde gerçekleştiriyoruz.

Bu geliştirmelerden sonra client uygulamasının ‘Startup.cs’ dosyasında aşağıdaki geliştirmenin yapılması gerekmektedir;

            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, _ =>
                {
                    _.LoginPath = new PathString("/home/login");
                });

Eklene cookie servisine şema adı olarak ‘Cookies’ değerini taşıyan CookieAuthenticationDefaults.AuthenticationScheme sabit verilmektedir. Dikkat ederseniz aynı sabit yukarıda login işlemi esnasında da ilgili alanlarda kullanılmaktadır.

Ve bir nazarınıza değer noktayı daha işaret ederim ki o da ‘AddOpenIdConnect’ servisinin kullanılmamasıdır. ‘AddOpenIdConnect’ servisi kimlik doğrulamayı gerçekleştiren bir protokoldü. Haliyle bizler bu protokolün yapacağı işlemi yukarıda manuel yapmış bulunmaktayız. O yüzden bu servisi eklemeye gerek kalmamıştır.

Ve son olarak client uygulamasında kimlik doğrulama ve yetkilendirme işlemlerinin sağlıklı bir şekilde işleyebilmesi için ‘UseAuthentication’ ve ‘UseAuthorization’ middleware’lerini çağırmayı unutmuyoruz.

            app.UseAuthentication();
            app.UseAuthorization();

Çıkış işlemi içinde aşağıdaki action’ı kullanabilirsiniz.

        public async Task Logout()
        {
            await HttpContext.SignOutAsync();
        }

Velhasıl kelam, artık uygulamayı derleyip, çalıştırabilir ve test edebiliriz.
IdentityServer4 Yazı Serisi #22 – Resource Owner Credential Grant Uygulaması
Görüldüğü üzere MVC uygulaması resource owner credential akışıyla gayet başarılı bir şekilde Auth Server’dan yetkilendirilebilmektedir.

Ayriyeten,
Angular vs. gibi SPA uygulamalarında yahut Console gibi farklı platformlarda resource owner credential kullanmak istiyorsanız eğer temelde kullanacağınız yapılanma yine aynı tasarımdan ibaret olacaktır. Sadece token’ı elde ettikten sonra authentication property’leri MVC’de olduğu gibi store etmenize gerek kalmayacak, hangi platformda çalışıyorsanız ilgili platformun gereği ne ise ona göre bir tasarım yapılması gerekecektir. Tabi, platform olarak Angular gibi SPA tercih ediyorsanız, uygulama ile Auth Server arasında ilgili akışı kullanacak bir API tasarlayabilir ve onun üzerinden Auth Server’la ilişkiyi kurabilirsiniz.

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

Not : Örnek uygulamayı indirebilmek için buraya tıklayınız.

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

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