.NET CORE ile RESTful Servislerde Basit Authentication İşlemi

Merhaba sevgili dostlar!

Güvenlik deyince aklınıza ne geliyor? Şahsen benim aklıma gelen ilk şey riskler oluyor. Hangi meslekte olursa olsun, bu durum geçerlidir sanırım. Hele ki bir sistem kuruyorsanız, güvenlik konusunda ne kadar paranoyak olursanız o kadar iyi.

Elbette konumuz bir .Net Core ile web API üzerinde güvenlik alt yapısına giriş yapmak ama ondan önce, dünya tarihinde güvenlik kavramının nasıl ele alındığına baksak iyi olur. Sonuçta biliyorsunuz ki yazılım geliştirirken kullandığımız hiçbir kavramı sıfırdan icat etmiyoruz. Dünya aleminde var olanı aktarıyoruz.

Dünya tarihinde güvenlik, insanlık tarihi kadar eski. Büyüklüğü ne olursa olsun bir alanı savunmak zorunda olan her insan, güvenlik önlemleri almak zorunda kalacaktır. Bu önlemler öncelikle saldırı tehlikesine karşıdır elbette. Ancak üzerine düşündükçe başka tehlikeler de ortaya çıkacaktır. Bunlardan biri (belki de en tehlikelisi) ajanlık, yani sızma faaliyetleridir.

Şimdi, bundan birkaç bin yıl önce yaklaşık 5000 kişilik, komşularıyla askeri ve ticari ilişkileri olan bir toplumun güvenliğinden sorumlu olduğunuzu hayal edin. Kasabanıza girenlerin, sizin toplumunuzun bir üyesi olup olmadığını nasıl anlayacaksınız? Hele ki bunlardan bazıları kendilerinin asker olduğunu iddia ediyorlarsa…

Bunun üzerine biraz düşünürseniz, eninde sonunda girenlerin kimliğini belirlemek için bir yöntem bulursunuz. İşte buradaki kimlik tespitinin teknik karşılığı authentication oluyor. Kimlik tespitinin en basit yöntemi de tüm vatandaşlarınızı bir biçimde kayıt altına almak ve sadece onlar ile sizin aranızda olabilecek gizli bir işaret belirlemektir. Bu işaret, bir kelime, bir sembol ya da başka herhangi bir şey olabilir. Yeter ki kişi bize kimliğini (identity) ve gerçekten o kişi olduğunu ispat edebilsin.

Kişinin kimliğini kanıtlamak için verdiği bilgiye parola dediğimizi biliyorsunuz. Genellikle dilimizde şifre ve parolayı eşanlamlı olarak kullanıyoruz fakat teknik olarak aslında farklı kavramlar.

İşte sevgili dostlar, bir RESTFul servisin güvenliğini sağlarken de benzer bir biçimde düşüneceğiz.

Aslında tam da bu noktada biraz REST ve RESTFul servis arasındaki farktan bahsedebiliriz.

REST ve RESTful

Her şeyden önce ilk vurgulamamız gereken şey, REST’in Web Servisleri için bir mimari olduğudur. Ya da başka bir değişle bir web servisinin nasıl yazılması gerektiğine dair prensipler bütünüdür. Basitçe bu prensipler şöyle söyler; istemciler belirli URL adreslerine (endpoint) GET, POST, PUT ve DELETE talepleri göndersin. Bu talepleri de bu mimariye uyumlu bir servis yanıtlasın. İşte bu REST mimarisine uyumlu olarak yazılan servise de RESTful servis adını veriyoruz.

REST, Representational State Transfer yani temsili durum transferi kelimelerinin kısaltmasıdır. Peki bu ne demek? REST mimarisi, geleneksel sunucu – istemci ilişkisinden daha farklıdır. Bu mimaride sunucu, istemcinin durum bilgisini tutmaz. İstemci daha önce talepte bulunmuş mu ya da kaç defa bulunmuş veya bu istemcinin kim olduğu gibi bilgilerden bahsediyoruz.

Peki o zaman haberleşmeyi nasıl sağlıyoruz? İstemci, veriyi .json ya da .xml formatlarında paketleyip sunucuya aktarıyor. Sunucu da vereceği yanıtı aynı biçimde paketleyerek istemciye iletiyor. İşte durum özetle bu. Eğer bu konu hakkında ileri okuma yapmak istiyorsanız buradan buyrun.

İşte biz bu RESTful servisimizi .NET Core ile gerçekleştireceğiz. Ya da bilinen diğer popüler adıyla .NET Core ile bir Web API geliştireceğiz.  

Evet. Artık projemizi geliştirmeye başlayabiliriz. Ben, Visual Studio 2019’un ASP.NET Core Web Application teması içinden API projesi oluşturarak başladım. Senaryomuz, mümkün olduğu kadar basit olsun. Örneğin sadece kayıtlı kullanıcılara yanıt veren bir Web API kurgulayalım.

İlk olarak, model nesnemizi oluşturalım.  

public class User
    {
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public Guid Id { get; set; }
        public string UserName { get; set; }
        public string Name { get; set; }
        public string LastName { get; set; }
        public string Password { get; set; }
        public string Email { get; set; }
        public DateTime CreatedDate { get; set; } = DateTime.Now;
    }


Veritabanı erişim aracı olarak da Entity Framework Core kullanacağım. Bu nedenle, gerekli bağımlılıkları yükledikten sonra DbContext nesnesini aşağıdaki gibi oluşturdum.

public class DemoDbContext : DbContext
    {
        public DemoDbContext(DbContextOptions<DemoDbContext> options) : base(options)
        {

        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<User>().
                HasData(new User
                {
                    UserName = "demouser",
                    Password = "demouser1",
                    Email = "demo@user.com",
                    Name = "demo",
                    LastName = "user",
                    Id = Guid.NewGuid()
                });
        }
        public DbSet<User> Users { get; set; }

    } 

DemoDbContext sınıfının OnModelCreating metodunu, veritabanına bir kayıt eklemesi için yapılandırdım. Buna teknik olarak “seeding” (tohumlama) diyoruz. Bu sayede, veritabanı oluştuktan sonra, en azından bir tane kullanıcımız olacak.

EF Core için gerekli konfigürasyonu yaptıktan sonra veritabanını oluşturdum.

Şimdi bir Controller sınıfına ihtiyacımız var ve bu proje için otomatik oluşturulmuş bir API Controller işimizi görür:

Evet. User modelim için, DemoDbContext sınıfını kullanacak biçimde Controller sınıfımı oluşturdum. Oluşan bu sınıfın da, GetUser metodunu kullanacağım. Bu API metodunun, sadece kayıtlı kullanıcılar tarafından kullanılabilmesini istiyoruz. Yani yapmam gereken şey, buraya bir talep (request) geldiğinde, talebi gönderen istemcinin yetkili olup olmadığını kontrol etmek.

Bunun için kullandığımız yöntem, Authorize attribute’ünden faydalanmak. Bunun için bu attribute’ü aşağıdaki gibi ekliyorum:

[Authorize(AuthenticationSchemes = "Basic")]
        [HttpGet("{id}")]       
        public async Task<ActionResult<User>> GetUser(Guid id)
        {

            User user = await _context.Users.FindAsync(id);

            if (user == null)
            {
                return NotFound();
            }

            return user;
        }

Burada, Authorize attrbute’nün parametresi olarak AuthenticationSchemes özelliğine bir değer atadığıma dikkat çekmek istiyorum. Bunun nedenini anlamak önemli. Tek başına Authorize attribute’ü sadece “yetkili” ya da “yetkili değil” kontrolü yapar. Peki ama buna nasıl karar verir? İşte buna karar verecek olan özelleştirilmiş Authentication yapısı olacak. Haliyle burada, AuthenticationSchemes değeri “Basic” olan bir kimlik doğrulama yöntemine yönlendirmiş oldum.

Mademki bu yönlendirmeyi yaptım o zaman hemen AuthenticationSchemes’i özelleştirme işlemine geçelim. İlk olarak projeme Security isminde bir klasör açıyorum ve aşağıdaki sınıfı ekliyorum:

public class BasicAuthenticationOption : AuthenticationSchemeOptions
    {
    }

İşte özelleştirmem tamamlandı. Fakat, SOLID prensiplerinden dolayı, http talebini burada denetlemeyeceğim. Onun yerine, bir BasicAuthenticationHandler sınıfı ekleyeceğim:

public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOption>
    {

    }

Bu sınıfı yazmaya başlamadan önce, ne yapmak istediğimizi bir kez daha gözden geçirelim. Bir istemci (örneğin SPA mimarisiyle yazılmış bir web arayüzü), http aracılığıyla API metodumuza bir talepte bulunur. Metot, çalışmadan önce istemcinin yetkili biri olup olmadığını kontrol etmek üzere talebi buraya yönlendirir.

O halde biz burada, http’den gelen istekte ihtiyacımız olan bilgilerin olup olmadığına ve bu bilgilerin bizdeki verilerle uyumlu olup olmadığına bakacağız demektir. Yani, gelen veride kullanıcıya ait olan bazı bilgiler arayacağız ve bu bilgileri veritabanındaki veriler ile karşılaştıracağız. Mademki veritabanı ile çalışacağız o zaman bu sınıfı da DemoDbContext nesnesi ile çalışır duruma getirmeliyim.

Sınıfı implemente ettiğinizde, size parametre içermeyen bir Constructor olmadığını söyleyecektir. Burada ihtiyaç duyulan parametreleri oluşturup, üst sınıfa geçiriyoruz.

private readonly DemoDbContext context;
public BasicAuthenticationHandler(IOptionsMonitor<BasicAuthenticationOption> options,
                                          ILoggerFactory logger,
                                          UrlEncoder urlEncoder,
                                          ISystemClock systemClock,
                                          DemoDbContext context) : base(options, logger, urlEncoder, systemClock)
        {
            this.context = context;
           
        }  

Sınıfı implemente ettikten sonra, HandleAuthenticateAsync() metodunun override edilmesi gerektiğini fark etmişsinizdir. İşte bu metot, istemciden gelen talebi inceleyecek, denetleyecek ve ihtiyacımız olan verileri içeriyorsa, bunları veritabanında karşılaştıracak.

 O zaman tam burada, istemcinin talebi nasıl göndereceği üzerinde duralım. Hatta, direkt istemci tarafına geçelim ve bu post işlemini gerçekleştirecek html sayfasını oluşturalım.

AJAX Request

Öncelikle projeme wwwroot klasörü ekledim ve bu klasörün altına index.html dosyasını oluşturdum.

Bu sayfa bir demo amacı taşıdığı için çok büyük bir işimiz yok. Sadece JQuery ile bir AJAX çağrısı gerçekleştireceğiz. O nedenle sayfaya yalnızca bir buton atıp, bu butonun click olayında gerekli işlemleri yaptım.

<body>
    <input type="button" name="name" value="Test" id="btnTest" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script>
        $(document).ready(function () {
            $('#btnTest').on('click', function () {

                let email = 'demo@user.com';
                let password = "demouser1";
                $.ajax({
                    url: 'https://localhost:44356/api/Users/95ae5c1f-ea18-4867-981a-e448744315fe',
                    type: "GET",
                    contentType: "application/json",
                    dataType: "json",                    
                    success: function (result) {
                        
                    },
                    error: function (err) {
                        if (err.status == 401) {
                            alert("Eposta veya şifre hatalı ya da bu sayfaya talep gönderme yetkiniz yok. ");
                        }
                    }
                });
            });
        });
    </script>
</body>

Burada, AJAX (aslında AJAJ – Asynchronous JavaScript and JSON 😊 ) çağrısının gideceği URL adresinin, benim lokalimdeki api adresi olduğuna dikkat çekmek istiyorum. Ayrıca URL’de çok önemli bir parça daha var. Sonundaki değer, benim Users tablomda bulunan UserId değeri. Yani bu projeyi siz de yapıyorsanız, kendi Users tablonuzdaki UserId değerini kullanmayı unutmayın.

Pekala; istemci sunucuya talebi iletirken hem kimlik bilgisini hem de bu bilginin kendisine ait olduğunu kanıtlayacak bir parolayı göndermesi gerektiğini biliyoruz. İşte bunun için kullandığımız yöntem, AJAX çağrısında headers özelliğini özelleştirmek. Bu özellik, kompleks bir özellik ve sunucuya anahtar – değer çiftleri ile veri aktarılabiliyor. Biz de burada headers özelliğine bir değer atayacağız. Authorization anahtarı ve karşılığında da “Basic” değeri ile birlikte email ve password değerlerini birleştirerek bir eşleştirme oluşturacağız.

Yalnız burada önemli bir durum daha var. Şifre ve kullanıcı gibi bilgilerin (claim) bu biçimde açık olarak gitmesi pek mantıklı değil sanki. Peki bunu nasıl gizleyebiliriz? JavaScript’de bunun en kolay yolu, ifadeyi Base64 String tipine çevirmek. İşte bunu da  btoa() fonksiyonu ile yapıyoruz. O halde istemcideki AJAX çağrısının son hali aşağıdaki gibi olacak:

$(document).ready(function () {
            $('#btnTest').on('click', function () {

                let email = 'demo@user.com';
                let password = "demouser1";
                $.ajax({
                    url: 'https://localhost:44356/api/Users/95ae5c1f-ea18-4867-981a-e448744315fe',
                    type: "GET",
                    contentType: "application/json",
                    dataType: "json",
                    headers: {
                        'Authorization': 'Basic ' + btoa(email + ':' + password)
                    },
                    success: function (result) {
                        
                    },
                    error: function (err) {
                        if (err.status == 401) {
                            alert("Eposta veya şifre hatalı ya da bu sayfaya talep gönderme yetkiniz yok. ");
                        }
                    }
                });
            });
        });

Artık, istemcinin talebi göndermesiyle başlayan yaşam döngüsü üzerinden mekanizmayı inceleyebiliriz. Yukarıda, JQuery’nin  ajax fonksiyonu ile gerçekleştirdiğimiz talep, sırasıyla aşağıdaki adımları atacak:

  1. UserController içinde yer alan GetUser action’una gidecek.
  2. GetUser metodu üzerinde belirttiğimiz [Authorize(AuthenticationSchemes = “Basic”)] attribute’ü ile BasicAuthenticationHandler sınıfına gönderilecek.
  3. Bu sınıfın HandleAuthenticateAsync metodu talebi yakalayacak ve incelemeye başlayacak.

Bu durumda HandleAuthenticateAsync metodunu yazmaya dönebiliriz. Öncelikle; gelen talebin Headers bilgisi içinde “Authorization” anahtar kelimesinin olup olmadığına bakmamız gerek. Eğer yoksa, bir sonuç döndürmeyeceğiz.

if (!Request.Headers.ContainsKey("Authorization"))
{
    return Task.FromResult(AuthenticateResult.NoResult());
}

Eğer “Authorization” ifadesi varsa, bu kez anahtar kelimenin değerini almamız gerek. Bunun için de AuthenticationHeaderValue.TryParse() metodunu kullanacağız. Eğer her şey yolundaysa, bu metot, header değerini saklayan bir değişken oluşturacak bize.

   if (!AuthenticationHeaderValue.TryParse(Request.Headers["Authorization"], out AuthenticationHeaderValue headerValue))
   {
       return Task.FromResult(AuthenticateResult.NoResult());

   }

  Eğer headerValue parametremiz varsa bu, Scheme (şema) bilgisini alabileceğimiz anlamına gelir. Eğer bu bilgi de “Basic” ise sorun yok demektir.

if (!headerValue.Scheme.Equals("Basic", StringComparison.OrdinalIgnoreCase))
{   
   return Task.FromResult(AuthenticateResult.NoResult());
}

Pekala! Buraya kadar gelen isteğin bizim belirlediğimiz değerleri içerdiğinden emin olduk. Şimdi bu değerler içinde bulunan eposta ve şifre aracılığıyla, kişinin id bilgisine ulaşmamız gerekiyor.

JavaScript tarafında; btoa fonksiyonu aracılığıyla, değerlerimizi Base64 String’e encode etmiştik. Demek ki bu kez encode edilmiş veriyi 8 bitlik String tipine dönüştürmemiz gerekecek. Bunu da basitçe aşağıdaki satırlar ile hallediyoruz.

byte[] headerValueBytes = Convert.FromBase64String(headerValue.Parameter);
string mailPassword = Encoding.UTF8.GetString(headerValueBytes);

Artık  mailPassword değişkeni içinde iki nokta üst üste (:) ile belirttiğim eposta ve şifre bilgileri olduğunu biliyorum. O zaman tek yapmamız gereken, bu değerleri alıp user nesnesinin varlığını doğrulamak.

string[] parts = mailPassword.Split(':');
string mail = parts[0];
string password = parts[1];
User user = context.Users.FirstOrDefault(x => x.Email == mail && x.Password == x.Password);

Bu noktada, burada yer alan mail ve userId gibi bilgilerin her birinin Claim olarak isimlendirildiğini belirtmiştik. Kullanıcı hakkında tutmak istediğiniz her veri; doğum tarihi, tuttuğu futbol takımı ya da telefon numarası birer claim’dir. Bunlar bir araya geldiklerinde ClaimsIdentity nesnesini oluştururlar ve bu nesne aracılığıyla, Windows Identity Foundation (WIF) tarafından temel kimlik denetleyicisi olarak kullanılan ClaimsPrincipal nesnesini üretebiliriz.

Claim[] claims = new[]
            {
                new Claim(ClaimTypes.Name, mail),
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())
            };

ClaimsIdentity identity = new ClaimsIdentity(claims, Scheme.Name);
ClaimsPrincipal principal = new ClaimsPrincipal(identity);

Artık elimizde ClaimsPrincipal nesnesi olacağına göre, kimliğinin doğrulandığına dair bir AuthenticationTicket verebiliriz. Bunun ardından da kimlik denetiminin başarılı olduğu bilgisini döndürebiliriz.

AuthenticationTicket ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));

Yani, metodumuzun son hali aşağıdaki gibi oldu:

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (!Request.Headers.ContainsKey("Authorization"))
            {
                return Task.FromResult(AuthenticateResult.NoResult());
            }
            if (!AuthenticationHeaderValue.TryParse(Request.Headers["Authorization"], out AuthenticationHeaderValue headerValue))
            {
                return Task.FromResult(AuthenticateResult.NoResult());

            }

            if (!headerValue.Scheme.Equals("Basic", StringComparison.OrdinalIgnoreCase))
            {
        
                return Task.FromResult(AuthenticateResult.NoResult());

            }

            byte[] headerValueBytes = Convert.FromBase64String(headerValue.Parameter);
            string mailPassword = Encoding.UTF8.GetString(headerValueBytes);
            string[] parts = mailPassword.Split(':');
            string mail = parts[0];
            string password = parts[1];

            User user = context.Users.FirstOrDefault(x => x.Email == mail && x.Password == x.Password);


            Claim[] claims = new[]
            {
                new Claim(ClaimTypes.Name, mail),
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())
            };

            ClaimsIdentity identity = new ClaimsIdentity(claims, Scheme.Name);
            ClaimsPrincipal principal = new ClaimsPrincipal(identity);


            AuthenticationTicket ticket = new AuthenticationTicket(principal, Scheme.Name);
            return Task.FromResult(AuthenticateResult.Success(ticket));
        }

Talep bu metoda gelip, biletini aldıktan sonra UserController’i içindeki GetUser metoduna dönecek. Burada; metoda gönderilen id değeri ile ClaimsIdentity’de bulunan id değerini karşılaştırmamız gerekiyor. Yani bir başka deyişle; trendeki bir yolcunun biletini denetleyen kondüktörün yapacağı işi yapacağız. Aşağıda, bu işlemlerin sonunda GetUser’ın geldiği son noktayı bulabilirsiniz:

        [Authorize(AuthenticationSchemes = "Basic")]
        [HttpGet("{id}")]
        public async Task<ActionResult<User>> GetUser(Guid id)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest();
            }

            ClaimsIdentity identity = User.Identity as ClaimsIdentity;
            string currentUserId = identity.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value;

            if (currentUserId != id.ToString())
            {
                return BadRequest("Kimliğiniz tespit edilemedi");
            }

            User user = await _context.Users.FindAsync(id);

            if (user == null)
            {
                return NotFound();
            }

            return user;
        }

Evet artık son aşamaya geçebiliriz. .NET Core’un Startup’unda bazı ayarlar yapmamız gerekecek.

Startup.cs Sınıfı

İlk olarak, Cross Origin Resource Sharing (CORS) ile domain dışından gelen taleplere izin verilmesini söyleyeceğiz. Bunun için aşağıdaki adımları atmak gerekiyor:

  • ConfigureServices() metodu
services.AddCors(x =>
                x.AddPolicy("AllowAll", x =>
                {
                    x.AllowAnyOrigin();
                    x.AllowAnyMethod();
                    x.AllowAnyHeader();
                })
            );    

Burada, “AllowAll” isminde bir kural oluşturduk. Bu kurala göre, “Herhangi bir orjinden her header bilgisine ve  her  metoda açık olduğumuzu” belirttik. Ayrıca elbette bu konfigürasyonu kullanmamız gerekecek.

  • Configure() metodu
app.UseCors("AllowAll");

Ardından; özelleştirdiğimiz Authentication yapısını burada sisteme kaydetmemiz gerekiyor ki, Authorize attribute’ünü kullandığım her metot doğru sınıfa yönlendirilebilsin. Ayrıca, dependency injection uygulanabilir hale getirmem gerekiyor. Bu nedenle aşağıdaki değişiklikleri de ekliyorum:

ConfigureServices() Metodu:

services.AddAuthentication("Basic").AddScheme<BasicAuthenticationOption, BasicAuthenticationHandler>("Basic", null);
services.AddTransient<IAuthenticationHandler, BasicAuthenticationHandler>();

Configure() Metodu

app.UseAuthorization();

 Son olarak; statik bir .html dosyası kullanmış olduğum için, bu sayfayı erişime açık hale getirmem gerekiyor. Bunun için de Configure() metoduna aşağıdaki satırı ekliyorum:

app.UseStaticFiles();     

Yani son değişiklikler ile birlikte Startup.cs sınıfının ilgili metotları aşağıdaki gibi olacak:

    public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<DemoDbContext>(opt => opt.UseSqlServer(Configuration.GetConnectionString("demoDb")));

            services.AddCors(x =>
                x.AddPolicy("AllowAll", x =>
                {
                    x.AllowAnyOrigin();
                    x.AllowAnyMethod();
                    x.AllowAnyHeader();
                })
            );

            services.AddAuthentication("Basic").AddScheme<BasicAuthenticationOption, BasicAuthenticationHandler>("Basic", null);
            services.AddTransient<IAuthenticationHandler, BasicAuthenticationHandler>();
            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();
            app.UseCors("AllowAll");
            app.UseRouting();

            app.UseAuthorization();

            app.UseStaticFiles();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

Evet sevgili dostlar! Bu yazımızda, .NET CORE ile RESTful servis geliştirirken temel Authentication’u nasıl özelleştireceğimizi ele aldık. Başka bir yazıda görüşmek ümidiyle.

Not: Projenin kaynak kodlarına buradan ulaşabilirsiniz:

https://github.com/turkayurkmez/BasicAuthenticationDEMO

Leave a Reply