.NET Core Microservices – Ocelot İle Authentication İşlemleri

Merhaba,

Önceki .NET Core Microservices – Ocelot API Gateway başlıklı makalemizde bir Gateway kütüphanesi olan Ocelot’un; microservice’ler arası geçiş görevi gören sorumluluğundan, deploy tasarımlarından, pratik olarak nasıl kullanıldığından ve daha birçok özelliğinden detaylıca bahsetmiş bulunmaktayız. Bu içeriğimizde ise Ocelot üzerinden mikro servislerimize erişim sağlarken kimlik doğrulamanın(authentication) nasıl gerçekleştirildiğini ele alacak ve pratikte uygulayacağız. Haydi başlayalım…

Ocelot İle Authentication Tasarımı

Ocelot API Gateway kütüphanesinde authentication kullanabilmek için token yapılanmasından faydalanacağız. Yapısal olarak, client’ın yapacağı giriş talebi neticesinde token’ı üretebilmek için yine Ocelot tarafından erişim sağlanan bir servis bu işi üstlenecek ve üretilen token’ı client’a gönderecektir. Ardından client, bu token’ı kullanarak diğer mikro servislere Ocelot üzerinden talepte bulunacak ve her bir servis token’ı doğrulayacaktır.
.NET Core Microservices - Ocelot İle Authentication İşlemleriYandaki görsel anlatılmak istenileni tam olarak özetleyecek mahiyettedir. İncelendiğinde görüleceği üzere servislerden sadece token üretiminden sorumlu bir ‘Auth Service’ mevcuttur(Bu servis IdentityServer4 serviside olabilir) Client sistemi kullanabilmek amacıyla kimliğini doğrulayabilmek için bu servisten bir token talebinde bulunmakta ve elde edilen bu token değeri ile diğer servislere(A Service, B Service…) request atmaktadır. Böylece authentication sağlanmış olunmaktadır. Tabi ki tüm bu süreç Ocelot aracılığıyle tek elden gerçekleştirilmektedir 🙂

Ocelot İle Authentication Uygulaması

Bir tasarımı anlamanın en iyi yolu onu uygulamaktır. Dolayısıyla bu içeriğimizde Ocelot ile authentication işlemlerinin nasıl uygulandığını anlayabilmek için pratik olarak tam teferruatıyla ele alacak ve örnek bir çalışma gerçekleştireceğiz.

Herşeyden önce ‘ProductAPI’, ‘CustomerAPI’, ‘AuthAPI’ ve ‘APIGateway’ isimlerinde dört adet proje oluşturalım.

dotnet new webapi --name ProductAPI
dotnet new webapi --name CustomerAPI
dotnet new webapi --name AuthAPI(JWT)
dotnet new webapi --name APIGateway(Ocelot)

Bu projelerden ‘APIGateway’de Ocelot ile microservice’lere geçiş işlemleri gerçekleştirecek, ‘AuthAPI’ ile kullanıcı doğrulama için token oluşturacak ve ‘ProductAPI’ ve ‘CustomerAPI’ ile de örnek işlevsel microservice çalışması gerçekleştireceğiz.

İlk olarak local’de çalışacağımızdan dolayı tüm projelerin portlarını çakışmaması için farklı olacak şekilde ayarlayarak başlayalım;

  • ProductAPI

        "ProductAPI": {
          "commandName": "Project",
          "launchBrowser": false,
          "applicationUrl": "https://localhost:1000;http://localhost:1001",
          "environmentVariables": {
            "ASPNETCORE_ENVIRONMENT": "Development"
          }
        }
    
  • CustomerAPI

        "CustomerAPI": {
          "commandName": "Project",
          "launchBrowser": false,
          "applicationUrl": "https://localhost:2000;http://localhost:2001",
          "environmentVariables": {
            "ASPNETCORE_ENVIRONMENT": "Development"
          }
        }
    
  • AuthAPI

        "AuthAPI": {
          "commandName": "Project",
          "launchBrowser": false,
          "applicationUrl": "https://localhost:3000;http://localhost:3001",
          "environmentVariables": {
            "ASPNETCORE_ENVIRONMENT": "Development"
          }
        }
    
  • APIGateway

        "APIGateway": {
          "commandName": "Project",
          "launchBrowser": false,
          "applicationUrl": "https://localhost:4000;http://localhost:4001",
          "environmentVariables": {
            "ASPNETCORE_ENVIRONMENT": "Development"
          }
        }
    

Ardından her bir servisi tek tek ele alarak kodlamaya başlayalım.

Öncelikle ‘APIGateway’ projesinden başlayalım. İlgili projede Ocelot’ın kurulumunu ve microservice’lere erişim için temel konfigürasyonu gerçekleştirelim. Bunun nasıl yapıldığını bilmiyorsanız eğer yukarılarda referans edilmiş .NET Core Microservices – Ocelot API Gateway başlıklı makaleye göz atmanız yeterli olacaktır.

Temel konfigürasyon(ocelot.json) dosyası aşağıdaki gibi olacaktır.

{
   "Routes": [
      {
         "DownstreamPathTemplate": "/api/products",
         "DownstreamScheme": "https",
         "DownstreamHostAndPorts": [
             {
                 "Host": "localhost",
                 "Port": 1000
             }
         ],
         "UpstreamPathTemplate": "/api/products",
         "UpstreamHttpMethod": [ "Get" ]
      },
      {
         "DownstreamPathTemplate": "/api/customers",
         "DownstreamScheme": "https",
         "DownstreamHostAndPorts": [
             {
                 "Host": "localhost",
                 "Port": 2000
             }
         ],
         "UpstreamPathTemplate": "/api/customers",
         "UpstreamHttpMethod": [ "Get" ]
      },
      {
         "DownstreamPathTemplate": "/api/auths",
         "DownstreamScheme": "https",
         "DownstreamHostAndPorts": [
             {
                 "Host": "localhost",
                 "Port": 3000
             }
         ],
         "UpstreamPathTemplate": "/api/auths",
         "UpstreamHttpMethod": [ "Get" ]
      }
   ],
   "GlobalConfiguration": {
       "BaseUrl": "https://localhost:4000"
   }
}

Tabi içeriğimizin devamında buradaki template’lere uygun controller sınıfları yeri geldikçe ilgili projelerde oluşturacağız.

İkinci olarak ‘AuthAPI’ projesinden devam edelim. Bu servis, sadece token üretiminden sorumlu olacaktır. Bundan dolayı sadece bu işi yapabilecek bir controller geliştirilmesi yeterlidir. Misal; bu controller ‘AuthsController’ isminde olabilir.

JWT üretimi için ‘Security/Secret Key’, ‘Issuer’, ‘Audience’ gibi değerler gerekecektir. Bunları sabit olarak ‘appsettings.json’ dosyasında aşağıdaki gibi tutabiliriz;

  "JWT": {
    "Security": "ocelot authentication example project",
    "Issuer": "https://localhost:4000",
    "Audience": "Ocelot Example"
  }

Ardından token üretimi için bir ‘TokenHandler’ sınıfı oluşturalım.

   public static class TokenHandler
   {
      public static IConfiguration _configuration;
      public static dynamic CreateAccessToken()
      {
         SymmetricSecurityKey symmetricSecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration["JWT:Security"]));
         TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
         {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = symmetricSecurityKey,
            ValidateIssuer = true,
            ValidIssuer = _configuration["JWT:Issuer"],
            ValidateAudience = true,
            ValidAudience = _configuration["JWT:Audience"],
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero,
            RequireExpirationTime = true
         };
         DateTime now = DateTime.UtcNow;
         JwtSecurityToken jwt = new JwtSecurityToken(
                  issuer: _configuration["JWT:Issuer"],
                  audience: _configuration["JWT:Audience"],
                  claims: new List<Claim> {
                         new Claim(ClaimTypes.Name, "gncy")
                  },
                  notBefore: now,
                  expires: now.Add(TimeSpan.FromMinutes(2)),
                  signingCredentials: new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256)
              );
         return new
         {
            AccessToken = new JwtSecurityTokenHandler().WriteToken(jwt),
            Expires = TimeSpan.FromMinutes(2).TotalSeconds
         };
      }
   }

Yukarıdaki kaynak kodun sağlıklı çalışabilmesi için ilgili projeye ‘Microsoft.IdentityModel.Tokens‘ ve ‘System.IdentityModel.Tokens.Jwt‘ kütüphanelerinin eklenmesi gerekmektedir. Bunun için aşağıdaki Dotnet CLI komutlarını kullanabilirsiniz.

dotnet add package Microsoft.IdentityModel.Tokens
dotnet add package System.IdentityModel.Tokens.Jwt

Oluşturulan ‘TokenHandler’ sınıfını kullanarak ‘AuthsController’ controller’ını aşağıdaki gibi geliştirelim.

   [ApiController]
   [Route("api/[controller]")]
   public class AuthsController : ControllerBase
   {
      IConfiguration _configuration;
      public AuthsController(IConfiguration configuration)
      {
         _configuration = configuration;
      }
      public IActionResult Login(string userName, string password)
      {
         TokenHandler._configuration = _configuration;
         return Ok(userName == "gncy" && password == "12345" ? TokenHandler.CreateAccessToken() : new UnauthorizedResult());
      }
   }

Burada ‘Login’ action’ında farazi bir kullanıcı adı ve şifre kontrolü yapmış bulunmakta ve geriye ya token değeri yahut ‘401’ status code’u dönmekteyiz.

‘AuthAPI’ projesinin gelişimini bu şekilde tamamlamış bulunmaktayız. Ocelot üzerinden ilgili projeyi tetikleyebilmek için aşağıdaki url’i kullanacağız.

https://localhost:4000/api/auths?userName=gncy&password=12345

Peki hocam, neden Startup.cs dosyasında Authentication servisini uygulamaya dahil etmedik?
‘AuthAPI’ projesi, yukarıda ifade ettiğimiz gibi sade ve sadece token üretiminden sorumlu bir servistir. ‘Startup.cs’ dosyasında Authentication servisini eklemek demek, bu uygulamada kimlik doğrulama işlemi yapılacak demektir. Halbuki bizler sadece token üreteceğiz. Amma velakin ‘ProductAPI’, ‘CustomerAPI’ ve ‘APIGateway’ sevislerinde kullanıcı denetimi sağlanacağı için ‘Startup.cs’ dosyasında authentication modülü yüklenecek ve böylece gelen isteklerde kimlik doğrulama işlemleri aktifleştirilmiş olacaktır.

‘AuthAPI’ projesini de geliştirdikten sonra sıra ‘ProductAPI’ ve ‘CustomerAPI’ projelerine gelmiş bulunmaktadır. Örneklendirmemizde authentication yapılanmasının çalışıp çalışmadığını daha net test edebilmek için bu projelerden birinde(ProductAPI) authorize ile kontrol işlemi gerçekleştirecek, diğerinde(CustomerAPI) ise serbest erişim sağlayacağız.

ProductAPI;

   [ApiController]
   [Route("api/[controller]")]
   [Authorize]
   public class ProductsController : ControllerBase
   {
      public IActionResult Get()
      {
         return Ok(new List<string> {
           "Telefon", "Terlik", "Kalem", "Kağıt", "Ampul", "Kağıt"
         });
      }
   }

Yukarıda bahsedildiği gibi ürün işlemlerini gerçekleştireceğimiz ‘ProductAPI’ içerisindeki oluşturulan bu farazi controller’da authorize işlemi yapılmaktadır. Dolayısıyla ilgili uygulama yapılan isteklerde bir kimlik doğrulaması gerçekleştirmek zorunda. Bunun için ‘Startup.cs’ dosyasında aşağıdaki gibi ‘AddAuthentication’ ile birlikte ‘AddJwtBearer’ servislerinin eklenmesi ve temel konfigürasyonlarının yapılması gerekmektedir;

   public class Startup
   {
      .
      .
      .
      public void ConfigureServices(IServiceCollection services)
      {
         SymmetricSecurityKey signInKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Security"]));
         string authenticationProviderKey = "TestKey";
         services.AddAuthentication(option => option.DefaultAuthenticateScheme = authenticationProviderKey)
             .AddJwtBearer(authenticationProviderKey, options =>
             {
                options.RequireHttpsMetadata = false;
                options.TokenValidationParameters = new TokenValidationParameters
                {
                   ValidateIssuerSigningKey = true,
                   IssuerSigningKey = signInKey,
                   ValidateIssuer = true,
                   ValidIssuer = _configuration["JWT:Issuer"],
                   ValidateAudience = true,
                   ValidAudience = _configuration["JWT:Audience"],
                   ValidateLifetime = true,
                   ClockSkew = TimeSpan.Zero,
                   RequireExpirationTime = true
                };
             });
         services.AddControllers();
      }

      public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
      {
         .
         .
         .
         app.UseAuthentication();
         app.UseAuthorization();
         .
         .
         .
      }
   }

Bu servisler sayesinde bizler uygulamaya kimlik doğrulama niteliğini kazandırmış bulunmaktayız. Ayrıca bu sevisleri ekleyebilmek için ‘AuthAPI’da olduğu gibi bu uygulamayada ‘Microsoft.IdentityModel.Tokens‘ ve ‘System.IdentityModel.Tokens.Jwt‘ kütüphanelerinin yanında Microsoft.AspNetCore.Authentication.JwtBearer kütüphanesinin eklenmesi gerekmektedir.

dotnet add package Microsoft.IdentityModel.Tokens
dotnet add package System.IdentityModel.Tokens.Jwt
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Velhasıl yukarıdaki kod bloğunu incelediğimizde, 11. satırdaki ‘AddJwtBearer’ servisine ‘authenticationProviderKey’ değişkenindeki ‘TestKey’ değerinin verildiğine dikkatinizi çekerim. Bu değer bizler için kimlik doğrulama sürecinde bir authentication scheme görevi görecektir. İçeriğimizin devamında ‘APIGateway’ projesi içinde yapacağımız birebir aynı konfigürasyonlar neticesinde, gelen ve Ocelot tarafından doğrulanacak isteği bu şema ile ayırt edebilecek ve ‘ProductAPI’ ile ilişkilendireceğiz. Böylece orada(APIGateway) doğrulanan token burada da(ProductAPI) doğrulanmış olacaktır. Ayriyetten 35 ve 36. satırlara göz atarsanız eğer ‘UseAuthentication’ ve ‘UseAuthorization’ middleware’leri çağrılarak uygulamada üzerine çalışılan tüm servisler işlevsel hale getirilmiş olmaktadır.

Son olarak ‘appsettings.json’ dosyasına aşağıdaki konfigürasyon datalarını yerleştirelim.

  "JWT": {
    "Security": "ocelot authentication example project",
    "Issuer": "https://localhost:4000",
    "Audience": "Ocelot Example"
  }

Burada dikkat edilmesi gereken mühim bir husus mevcuttur! O da şudur ki, token değerini üreten servisteki(AuthAPI) konfigürasyon değerleri ile bu token’ı kullanacak olan tüm servislerdeki konfigürasyon değerlerinin birebir aynı olması gerektiğidir. Bunun nedeni, doğrulama esnasında ilgili servisin gelen token’da ki şifrelenmiş datalarının geçerliliğini verilen datalarla check etmesidir.

Token üreten servis ile o token’ı kullanan servisler aynı konfigürasyon değerlerini baz almalıdırlar.

CustomerAPI;
‘CustomerAPI’ uygulaması içerisinde ise aşağıdaki gibi bir controller tasarlanması içeriğimiz açısından oldukça yeterli olacaktır;

   [ApiController]
   [Route("api/[controller]")]
   public class CustomersController : ControllerBase
   {
      public IActionResult Get()
      {
         return Ok(new List<string> {
            "Hilmi", "Hüseyin", "Rıfkı", "Necati", "Şuayip", "Muallim", "Muiddin"
         });
      }
   }

İlgili serviste herhangi bir authentication işlemi yapmayacağımızı yukarıdaki satırlarda belirttiğimizden dolayı geliştirilmesi bu kadardır…

Şimdi oluşturduğumuz servisleri test etmek için hepsini dotnet build komutu ile derleyelim ve ardından dotnet run komutuyla ayağa kaldıralım. Ardından Postman uygulaması aracılığıyla her bir servise ayrı ayrı ve biryandan da Ocelot aracılığıyla isteklerde bulunalım. Böylece geliştirmede yapmamız gereken eksiklikleri daha rahat görebilecek ve tarafımca izah edebileceğiz.
Ayrı ayrı yapılan istekler;
.NET Core Microservices - Ocelot İle Authentication İşlemleri
Yukarıdaki ekran görüntüsünü incelerseniz eğer ‘ProductAPI’ servisi dışındaki diğer servisler kusursuz çalışmaktadırlar. Söz konusu servis ise yapılan authentication konfigürasyonundan dolayı şöyle bir hata vermektedir;


System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions).

Bu hatanın kaynağı ilgili serviste yapılan authentication konfigürasyonunun ‘TestKey’ gibi farklı bir şema ismiyle isimlendirilmesidir. Eğer ki şema adı ‘Bearer’ olsaydı bu hata ortadan kalkacak lakin herhangi bir token ile talepte bulunulmadığından dolayı klasik 401 hatası karşımıza gelecekti. Şimdi yapılan bu çalışmayı Ocelot üzerinden test edelim ve tek tek tüm servislere istek gönderelim.

Ocelot aracılığıyla yapılan istekler;
.NET Core Microservices - Ocelot İle Authentication İşlemleri
Yukarıdaki ekran görüntüsünü de incelerseniz eğer Ocelot üzerinden(APIGateway) yapılan tüm istek yönlendirmelerinde yine sadece ‘ProductAPI’ hata vermektedir. Hatanın kaynağı ve mesajı öncekiyle birebir aynıdır. Haliyle, istek Ocelot’dan da gelse değişen bişey olmamakta ve tekrardan hatayla karşılaşılmaktadır. İşte bu durumda Ocelot üzerinden şemaya uygun authorization konfigürasyonunun yapılması yeterli olacak ve hata ortadan kaldırılacaktır. Bunun için ‘APIGateway’ projesindeki ‘ocelot.json’ dosyasında aşağıdaki ayarlamanın yapılması gerekmektedir;

{
   "Routes": [
      {
         "DownstreamPathTemplate": "/api/products",
         "DownstreamScheme": "https",
         "DownstreamHostAndPorts": [
             {
                 "Host": "localhost",
                 "Port": 1000
             }
         ],
         "UpstreamPathTemplate": "/api/products",
         "UpstreamHttpMethod": [ "Get" ],
         "AuthenticationOptions": {
            "AuthenticationProviderKey": "TestKey",
            "AllowedScopes": []
          }
      },
      {
         "DownstreamPathTemplate": "/api/customers",
         "DownstreamScheme": "https",
         "DownstreamHostAndPorts": [
             {
                 "Host": "localhost",
                 "Port": 2000
             }
         ],
         "UpstreamPathTemplate": "/api/customers",
         "UpstreamHttpMethod": [ "Get" ]
      },
      {
         "DownstreamPathTemplate": "/api/auths",
         "DownstreamScheme": "https",
         "DownstreamHostAndPorts": [
             {
                 "Host": "localhost",
                 "Port": 3000
             }
         ],
         "UpstreamPathTemplate": "/api/auths",
         "UpstreamHttpMethod": [ "Get" ]
      }
   ],
   "GlobalConfiguration": {
       "BaseUrl": "https://localhost:4000"
   }
}

Burada 14 ile 17. satırlar arasına dikkatimizi çekersek eğer ‘ProductAPI’ route’una özel bir ‘AuthenticationOptions’ tanımlandığını göreceğiz. Hemen ilgili alanı hususi olarak aşağıya alırsak eğer;

         "AuthenticationOptions": {
            "AuthenticationProviderKey": "TestKey",
            "AllowedScopes": []
          }

görüldüğü üzere ilgili route’a bir authentication konfigürasyonu sağlamaktadır. ‘AuthenticationProviderKey’ alanı ile hangi şema üzerinden kimlik doğrulama işlemi yapılacağı bildirilmektedir. Ayriyetten bu tanımlamanın sade ve sadece authentication işlemi yapılan route’lara yapılması gerektiğini özellikle vurgulamakta fayda görmekteyim.

Bu konfigürasyondan sonra ‘APIGateway’ uygulamasına da aynı şema adında authentication servisinin dahil edilmesi gerekmektedir;

   public class Startup
   {
      .
      .
      .
      public void ConfigureServices(IServiceCollection services)
      {
         SymmetricSecurityKey signInKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Security"]));

         string authenticationProviderKey = "TestKey";
         services.AddAuthentication(option => option.DefaultAuthenticateScheme = authenticationProviderKey)
             .AddJwtBearer(authenticationProviderKey, options =>
             {
                options.RequireHttpsMetadata = false;
                options.TokenValidationParameters = new TokenValidationParameters
                {
                   ValidateIssuerSigningKey = true,
                   IssuerSigningKey = signInKey,
                   ValidateIssuer = true,
                   ValidIssuer = _configuration["JWT:Issuer"],
                   ValidateAudience = true,
                   ValidAudience = _configuration["JWT:Audience"],
                   ValidateLifetime = true,
                   ClockSkew = TimeSpan.Zero,
                   RequireExpirationTime = true
                };
             });
         services.AddOcelot();
         .
         .
         .
      }

      public async void Configure(IApplicationBuilder app, IWebHostEnvironment env)
      {
         .
         .
         .
         app.UseAuthentication();
         app.UseAuthorization();
         await app.UseOcelot();
         .
         .
         .
      }
   }

Tüm bu geliştirmelerden sonra servisleri derleyip, çalıştırdığımızda aşağıdaki gibi bir işlevsellikle karşılaşacağız.
.NET Core Microservices - Ocelot İle Authentication İşlemleri
Görüldüğü üzere istek neticesinde token geçerliyse hedef data, aksi taktirde 401 durum kodu döndürülmektedir.

Alternatif Tasarım

Tüm servislerde authentication konfigürasyonu için şemaya alternatif olarak default değeri kullanabilirsiniz.

         SymmetricSecurityKey signInKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Security"]));

         services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
             .AddJwtBearer( options =>
             {
                options.RequireHttpsMetadata = false;
                options.TokenValidationParameters = new TokenValidationParameters
                {
                   ValidateIssuerSigningKey = true,
                   IssuerSigningKey = signInKey,
                   ValidateIssuer = true,
                   ValidIssuer = _configuration["JWT:Issuer"],
                   ValidateAudience = true,
                   ValidAudience = _configuration["JWT:Audience"],
                   ValidateLifetime = true,
                   ClockSkew = TimeSpan.Zero,
                   RequireExpirationTime = true
                };
             });
         "AuthenticationOptions": {
            "AuthenticationProviderKey": "Bearer",
            "AllowedScopes": []
          }

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

Not: Örnek uygulamayı indirmek 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

*